{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|default_exp data.preprocessing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Data preprocessing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">Functions used to preprocess time series (both X and y)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "from __future__ import annotations\n",
    "\n",
    "import re\n",
    "\n",
    "import sklearn\n",
    "from fastai.data.load import DataLoader\n",
    "from fastai.data.transforms import Categorize\n",
    "from fastai.tabular.core import df_shrink_dtypes, make_date\n",
    "from fasttransform import ItemTransform, Pipeline, Transform\n",
    "from joblib import dump, load\n",
    "from pandas._libs.tslibs.timestamps import Timestamp\n",
    "from sklearn.base import BaseEstimator, TransformerMixin\n",
    "\n",
    "from tsai.data.core import *\n",
    "from tsai.data.preparation import *\n",
    "from tsai.imports import *\n",
    "from tsai.utils import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tsai.data.external import get_UCR_data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dsid = 'NATOPS'\n",
    "X, y, splits = get_UCR_data(dsid, return_split=False)\n",
    "tfms = [None, Categorize()]\n",
    "dsets = TSDatasets(X, y, tfms=tfms, splits=splits)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class ToNumpyCategory(Transform):\n",
    "    \"Categorize a numpy batch\"\n",
    "    order = 90\n",
    "\n",
    "    def __init__(self, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: np.ndarray):\n",
    "        self.type = type(o)\n",
    "        self.cat = Categorize()\n",
    "        self.cat.setup(o)\n",
    "        self.vocab = self.cat.vocab\n",
    "        return np.asarray(stack([self.cat(oi) for oi in o]))\n",
    "\n",
    "    def decodes(self, o: np.ndarray):\n",
    "        return stack([self.cat.decode(oi) for oi in o])\n",
    "\n",
    "    def decodes(self, o: torch.Tensor):\n",
    "        return stack([self.cat.decode(oi) for oi in o])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([3, 2, 2, 3, 2, 4, 0, 5, 2, 1])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = ToNumpyCategory()\n",
    "y_cat = t(y)\n",
    "y_cat[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(t.decode(tensor(y_cat)), y)\n",
    "test_eq(t.decode(np.array(y_cat)), y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class OneHot(Transform):\n",
    "    \"One-hot encode/ decode a batch\"\n",
    "    order = 90\n",
    "    def __init__(self, n_classes=None, **kwargs):\n",
    "        self.n_classes = n_classes\n",
    "        super().__init__(**kwargs)\n",
    "    def encodes(self, o: torch.Tensor):\n",
    "        if not self.n_classes: self.n_classes = len(np.unique(o))\n",
    "        return torch.eye(self.n_classes)[o]\n",
    "    def encodes(self, o: np.ndarray):\n",
    "        o = ToNumpyCategory()(o)\n",
    "        if not self.n_classes: self.n_classes = len(np.unique(o))\n",
    "        return np.eye(self.n_classes)[o]\n",
    "    def decodes(self, o: torch.Tensor): return torch.argmax(o, dim=-1)\n",
    "    def decodes(self, o: np.ndarray): return np.argmax(o, axis=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0., 0., 0., 1., 0., 0.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 0., 1., 0., 0.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 1., 0.],\n",
       "       [1., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 1.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 1., 0., 0., 0., 0.]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "oh_encoder = OneHot()\n",
    "y_cat = ToNumpyCategory()(y)\n",
    "oht = oh_encoder(y_cat)\n",
    "oht[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_classes = 10\n",
    "n_samples = 100\n",
    "\n",
    "t = torch.randint(0, n_classes, (n_samples,))\n",
    "oh_encoder = OneHot()\n",
    "oht = oh_encoder(t)\n",
    "test_eq(oht.shape, (n_samples, n_classes))\n",
    "test_eq(torch.argmax(oht, dim=-1), t)\n",
    "test_eq(oh_encoder.decode(oht), t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_classes = 10\n",
    "n_samples = 100\n",
    "\n",
    "a = np.random.randint(0, n_classes, (n_samples,))\n",
    "oh_encoder = OneHot()\n",
    "oha = oh_encoder(a)\n",
    "test_eq(oha.shape, (n_samples, n_classes))\n",
    "test_eq(np.argmax(oha, axis=-1), a)\n",
    "test_eq(oh_encoder.decode(oha), a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSNan2Value(Transform):\n",
    "    \"Replaces any nan values by a predefined value or median\"\n",
    "    order = 90\n",
    "    def __init__(self, value=0, median=False, by_sample_and_var=True, sel_vars=None):\n",
    "        store_attr()\n",
    "        if not ismin_torch(\"1.8\"):\n",
    "            raise ValueError('This function only works with Pytorch>=1.8.')\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.sel_vars is not None:\n",
    "            mask = torch.isnan(o[:, self.sel_vars])\n",
    "            if mask.any() and self.median:\n",
    "                if self.by_sample_and_var:\n",
    "                    median = torch.nanmedian(o[:, self.sel_vars], dim=2, keepdim=True)[0].repeat(1, 1, o.shape[-1])\n",
    "                    o[:, self.sel_vars][mask] = median[mask]\n",
    "                else:\n",
    "                    o[:, self.sel_vars] = torch.nan_to_num(o[:, self.sel_vars], torch.nanmedian(o[:, self.sel_vars]))\n",
    "            o[:, self.sel_vars] = torch.nan_to_num(o[:, self.sel_vars], self.value)\n",
    "        else:\n",
    "            mask = torch.isnan(o)\n",
    "            if mask.any() and self.median:\n",
    "                if self.by_sample_and_var:\n",
    "                    median = torch.nanmedian(o, dim=2, keepdim=True)[0].repeat(1, 1, o.shape[-1])\n",
    "                    o[mask] = median[mask]\n",
    "                else:\n",
    "                    o = torch.nan_to_num(o, torch.nanmedian(o))\n",
    "            o = torch.nan_to_num(o, self.value)\n",
    "        return o\n",
    "\n",
    "\n",
    "\n",
    "Nan2Value = TSNan2Value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "o = TSTensor(torch.randn(16, 10, 100))\n",
    "o[0,0] = float('nan')\n",
    "o[o > .9] = float('nan')\n",
    "o[[0,1,5,8,14,15], :, -20:] = float('nan')\n",
    "nan_vals1 = torch.isnan(o).sum()\n",
    "o2 = Pipeline(TSNan2Value(), split_idx=0)(o.clone())\n",
    "o3 = Pipeline(TSNan2Value(median=True, by_sample_and_var=True), split_idx=0)(o.clone())\n",
    "o4 = Pipeline(TSNan2Value(median=True, by_sample_and_var=False), split_idx=0)(o.clone())\n",
    "nan_vals2 = torch.isnan(o2).sum()\n",
    "nan_vals3 = torch.isnan(o3).sum()\n",
    "nan_vals4 = torch.isnan(o4).sum()\n",
    "test_ne(nan_vals1, 0)\n",
    "test_eq(nan_vals2, 0)\n",
    "test_eq(nan_vals3, 0)\n",
    "test_eq(nan_vals4, 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "o = TSTensor(torch.randn(16, 10, 100))\n",
    "o[o > .9] = float('nan')\n",
    "o = TSNan2Value(median=True, sel_vars=[0,1,2,3,4])(o)\n",
    "test_eq(torch.isnan(o[:, [0,1,2,3,4]]).sum().item(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSStandardize(Transform):\n",
    "    \"\"\"Standardizes batch of type `TSTensor`\n",
    "\n",
    "    Args:\n",
    "        - mean: you can pass a precalculated mean value as a torch tensor which is the one that will be used, or leave as None, in which case\n",
    "            it will be estimated using a batch.\n",
    "        - std: you can pass a precalculated std value as a torch tensor which is the one that will be used, or leave as None, in which case\n",
    "            it will be estimated using a batch. If both mean and std values are passed when instantiating TSStandardize, the rest of arguments won't be used.\n",
    "        - by_sample: if True, it will calculate mean and std for each individual sample. Otherwise based on the entire batch.\n",
    "        - by_var:\n",
    "            * False: mean and std will be the same for all variables.\n",
    "            * True: a mean and std will be be different for each variable.\n",
    "            * a list of ints: (like [0,1,3]) a different mean and std will be set for each variable on the list. Variables not included in the list\n",
    "            won't be standardized.\n",
    "            * a list that contains a list/lists: (like[0, [1,3]]) a different mean and std will be set for each element of the list. If multiple elements are\n",
    "            included in a list, the same mean and std will be set for those variable in the sublist/s. (in the example a mean and std is determined for\n",
    "            variable 0, and another one for variables 1 & 3 - the same one). Variables not included in the list won't be standardized.\n",
    "        - by_step: if False, it will standardize values for each time step.\n",
    "        - exc_vars: list of variables that won't be standardized.\n",
    "        - eps: it avoids dividing by 0\n",
    "        - use_single_batch: if True a single training batch will be used to calculate mean & std. Else the entire training set will be used.\n",
    "    \"\"\"\n",
    "\n",
    "    parameters, order = L('mean', 'std'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, mean=None, std=None, by_sample=False, by_var=False, by_step=False, exc_vars=None, eps=1e-8, use_single_batch=True, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.mean = tensor(mean) if mean is not None else None\n",
    "        self.std = tensor(std) if std is not None else None\n",
    "        self._setup = (mean is None or std is None) and not by_sample\n",
    "        self.eps = eps\n",
    "        self.by_sample, self.by_var, self.by_step = by_sample, by_var, by_step\n",
    "        drop_axes = []\n",
    "        if by_sample: drop_axes.append(0)\n",
    "        if by_var: drop_axes.append(1)\n",
    "        if by_step: drop_axes.append(2)\n",
    "        self.exc_vars = exc_vars\n",
    "        self.axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes])\n",
    "        if by_var and is_listy(by_var):\n",
    "            self.list_axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes]) + (1,)\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.verbose = verbose\n",
    "        if self.mean is not None or self.std is not None:\n",
    "            pv(f'{self.__class__.__name__} mean={self.mean}, std={self.std}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "               self.verbose)\n",
    "\n",
    "    @classmethod\n",
    "    def from_stats(cls, mean, std): return cls(mean, std)\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                mean = torch.zeros(*shape, device=o.device)\n",
    "                std = torch.ones(*shape, device=o.device)\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    mean[:, v] = torch_nanmean(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True)\n",
    "                    std[:, v] = torch.clamp_min(torch_nanstd(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True), self.eps)\n",
    "            else:\n",
    "                mean = torch_nanmean(o, dim=self.axes, keepdim=self.axes!=())\n",
    "                std = torch.clamp_min(torch_nanstd(o, dim=self.axes, keepdim=self.axes!=()), self.eps)\n",
    "            if self.exc_vars is not None:\n",
    "                mean[:, self.exc_vars] = 0.\n",
    "                std[:, self.exc_vars] = 1.\n",
    "            self.mean, self.std = mean, std\n",
    "            if len(self.mean.shape) == 0:\n",
    "                pv(f'{self.__class__.__name__} mean={self.mean}, std={self.std}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "                   self.verbose)\n",
    "            else:\n",
    "                pv(f'{self.__class__.__name__} mean shape={self.mean.shape}, std shape={self.std.shape}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "                   self.verbose)\n",
    "            self._setup = False\n",
    "        elif self.by_sample: self.mean, self.std = torch.zeros(1), torch.ones(1)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.by_sample:\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                mean = torch.zeros(*shape, device=o.device)\n",
    "                std = torch.ones(*shape, device=o.device)\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    mean[:, v] = torch_nanmean(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True)\n",
    "                    std[:, v] = torch.clamp_min(torch_nanstd(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True), self.eps)\n",
    "            else:\n",
    "                mean = torch_nanmean(o, dim=self.axes, keepdim=self.axes!=())\n",
    "                std = torch.clamp_min(torch_nanstd(o, dim=self.axes, keepdim=self.axes!=()), self.eps)\n",
    "            if self.exc_vars is not None:\n",
    "                mean[:, self.exc_vars] = 0.\n",
    "                std[:, self.exc_vars] = 1.\n",
    "            self.mean, self.std = mean, std\n",
    "        return (o - self.mean) / self.std\n",
    "\n",
    "    def decodes(self, o:TSTensor):\n",
    "        if self.mean is None or self.std is None: return o\n",
    "        return o * self.std + self.mean\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms=[TSStandardize(by_sample=True, by_var=False, verbose=True)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, batch_tfms=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([ 0.0000, -1.3490,  0.0000,  0.9758, -0.8313, -0.4255,  0.0000, -0.6158,\n",
      "         0.0000,  0.7750, -0.4852, -0.0974,  0.0000, -1.0725, -0.6165,  0.9144,\n",
      "        -0.6939, -0.3040, -0.5606, -1.2007, -0.7442,  0.9320, -0.7604, -0.4319],\n",
      "       device='mps:0')\n",
      "tensor([1.0000, 0.8644, 1.0000, 0.7387, 1.1840, 0.5310, 1.0000, 0.2577, 1.0000,\n",
      "        0.2303, 0.4160, 0.3359, 1.0000, 0.6274, 0.2830, 0.5175, 0.8867, 0.4302,\n",
      "        0.5809, 0.7505, 0.3193, 0.6618, 1.0351, 0.4950], device='mps:0')\n"
     ]
    }
   ],
   "source": [
    "exc_vars = [0, 2, 6, 8, 12]\n",
    "batch_tfms=[TSStandardize(by_var=True, exc_vars=exc_vars)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, batch_tfms=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_eq(len(dls.train.after_batch.fs[0].mean.flatten()), 24)\n",
    "test_eq(len(dls.train.after_batch.fs[0].std.flatten()), 24)\n",
    "test_eq(dls.train.after_batch.fs[0].mean.flatten()[exc_vars].cpu(), torch.zeros(len(exc_vars)))\n",
    "test_eq(dls.train.after_batch.fs[0].std.flatten()[exc_vars].cpu(), torch.ones(len(exc_vars)))\n",
    "print(dls.train.after_batch.fs[0].mean.flatten().data)\n",
    "print(dls.train.after_batch.fs[0].std.flatten().data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tsai.data.validation import TimeSplitter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_nan = np.random.rand(100, 5, 10)\n",
    "idxs = random_choice(len(X_nan), int(len(X_nan)*.5), False)\n",
    "X_nan[idxs, 0] = float('nan')\n",
    "idxs = random_choice(len(X_nan), int(len(X_nan)*.5), False)\n",
    "X_nan[idxs, 1, -10:] = float('nan')\n",
    "batch_tfms = TSStandardize(by_var=True)\n",
    "dls = get_ts_dls(X_nan, batch_tfms=batch_tfms, splits=TimeSplitter(show_plot=False)(range_of(X_nan)))\n",
    "test_eq(torch.isnan(dls.after_batch[0].mean).sum(), 0)\n",
    "test_eq(torch.isnan(dls.after_batch[0].std).sum(), 0)\n",
    "xb = first(dls.train)[0]\n",
    "test_ne(torch.isnan(xb).sum(), 0)\n",
    "test_ne(torch.isnan(xb).sum(), torch.isnan(xb).numel())\n",
    "batch_tfms = [TSStandardize(by_var=True), Nan2Value()]\n",
    "dls = get_ts_dls(X_nan, batch_tfms=batch_tfms, splits=TimeSplitter(show_plot=False)(range_of(X_nan)))\n",
    "xb = first(dls.train)[0]\n",
    "test_eq(torch.isnan(xb).sum(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms=[TSStandardize(by_sample=True, by_var=False, verbose=False)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)\n",
    "xb, yb = next(iter(dls.valid))\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfms = [None, TSClassification()]\n",
    "batch_tfms = TSStandardize(by_sample=True)\n",
    "dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=[64, 128], inplace=True)\n",
    "xb, yb = dls.train.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)\n",
    "xb, yb = dls.valid.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfms = [None, TSClassification()]\n",
    "batch_tfms = TSStandardize(by_sample=True, by_var=False, verbose=False)\n",
    "dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=[64, 128], inplace=False)\n",
    "xb, yb = dls.train.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)\n",
    "xb, yb = dls.valid.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "\n",
    "@patch\n",
    "def mul_min(x:torch.Tensor|TSTensor|NumpyTensor, axes=(), keepdim=False):\n",
    "    if axes == (): return retain_type(x.min(), x)\n",
    "    axes = reversed(sorted(axes if is_listy(axes) else [axes]))\n",
    "    min_x = x\n",
    "    for ax in axes: min_x, _ = min_x.min(ax, keepdim)\n",
    "    return retain_type(min_x, x)\n",
    "\n",
    "\n",
    "@patch\n",
    "def mul_max(x:torch.Tensor|TSTensor|NumpyTensor, axes=(), keepdim=False):\n",
    "    if axes == (): return retain_type(x.max(), x)\n",
    "    axes = reversed(sorted(axes if is_listy(axes) else [axes]))\n",
    "    max_x = x\n",
    "    for ax in axes: max_x, _ = max_x.max(ax, keepdim)\n",
    "    return retain_type(max_x, x)\n",
    "\n",
    "\n",
    "class TSNormalize(Transform):\n",
    "    \"Normalizes batch of type `TSTensor`\"\n",
    "    parameters, order = L('min', 'max'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, min=None, max=None, range=(-1, 1), by_sample=False, by_var=False, by_step=False, clip_values=True,\n",
    "                 use_single_batch=True, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.min = tensor(min) if min is not None else None\n",
    "        self.max = tensor(max) if max is not None else None\n",
    "        self._setup = (self.min is None and self.max is None) and not by_sample\n",
    "        self.range_min, self.range_max = range\n",
    "        self.by_sample, self.by_var, self.by_step = by_sample, by_var, by_step\n",
    "        drop_axes = []\n",
    "        if by_sample: drop_axes.append(0)\n",
    "        if by_var: drop_axes.append(1)\n",
    "        if by_step: drop_axes.append(2)\n",
    "        self.axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes])\n",
    "        if by_var and is_listy(by_var):\n",
    "            self.list_axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes]) + (1,)\n",
    "        self.clip_values = clip_values\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.verbose = verbose\n",
    "        if self.min is not None or self.max is not None:\n",
    "            pv(f'{self.__class__.__name__} min={self.min}, max={self.max}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n', self.verbose)\n",
    "\n",
    "    @classmethod\n",
    "    def from_stats(cls, min, max, range_min=0, range_max=1): return cls(min, max, range_min, range_max)\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                _min = torch.zeros(*shape, device=o.device) + self.range_min\n",
    "                _max = torch.zeros(*shape, device=o.device) + self.range_max\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    _min[:, v] = o[:, v].mul_min(self.axes if len(v) == 1 else self.list_axes, keepdim=self.axes!=())\n",
    "                    _max[:, v] = o[:, v].mul_max(self.axes if len(v) == 1 else self.list_axes, keepdim=self.axes!=())\n",
    "            else:\n",
    "                _min, _max = o.mul_min(self.axes, keepdim=self.axes!=()), o.mul_max(self.axes, keepdim=self.axes!=())\n",
    "            self.min, self.max = _min, _max\n",
    "            if len(self.min.shape) == 0:\n",
    "                pv(f'{self.__class__.__name__} min={self.min}, max={self.max}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "                   self.verbose)\n",
    "            else:\n",
    "                pv(f'{self.__class__.__name__} min shape={self.min.shape}, max shape={self.max.shape}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "                   self.verbose)\n",
    "            self._setup = False\n",
    "        elif self.by_sample: self.min, self.max = -torch.ones(1), torch.ones(1)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.by_sample:\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                _min = torch.zeros(*shape, device=o.device) + self.range_min\n",
    "                _max = torch.ones(*shape, device=o.device) + self.range_max\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    _min[:, v] = o[:, v].mul_min(self.axes, keepdim=self.axes!=())\n",
    "                    _max[:, v] = o[:, v].mul_max(self.axes, keepdim=self.axes!=())\n",
    "            else:\n",
    "                _min, _max = o.mul_min(self.axes, keepdim=self.axes!=()), o.mul_max(self.axes, keepdim=self.axes!=())\n",
    "            self.min, self.max = _min, _max\n",
    "        output = ((o - self.min) / (self.max - self.min)) * (self.range_max - self.range_min) + self.range_min\n",
    "        if self.clip_values:\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    output[:, v] = torch.clamp(output[:, v], self.range_min, self.range_max)\n",
    "            else:\n",
    "                output = torch.clamp(output, self.range_min, self.range_max)\n",
    "        return output\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms = [TSNormalize()]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb.max() <= 1\n",
    "assert xb.min() >= -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms=[TSNormalize(by_sample=True, by_var=False, verbose=False)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb.max() <= 1\n",
    "assert xb.min() >= -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms = [TSNormalize(by_var=[0, [1, 2]], use_single_batch=False, clip_values=False, verbose=False)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb[:, [0, 1, 2]].max() <= 1\n",
    "assert xb[:, [0, 1, 2]].min() >= -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSStandardizeTuple(ItemTransform):\n",
    "    \"Standardizes X (and y if provided)\"\n",
    "    parameters, order = L('x_mean', 'x_std', 'y_mean', 'y_std'), 90\n",
    "\n",
    "    def __init__(self, x_mean, x_std, y_mean=None, y_std=None, eps=1e-5):\n",
    "        self.x_mean, self.x_std = torch.as_tensor(x_mean).float(), torch.as_tensor(x_std + eps).float()\n",
    "        self.y_mean = self.x_mean if y_mean is None else torch.as_tensor(y_mean).float()\n",
    "        self.y_std = self.x_std if y_std is None else torch.as_tensor(y_std + eps).float()\n",
    "\n",
    "    def encodes(self, xy):\n",
    "        if len(xy) == 2:\n",
    "            x, y = xy\n",
    "            x = (x - self.x_mean) / self.x_std\n",
    "            y = (y - self.y_mean) / self.y_std\n",
    "            return (x, y)\n",
    "        elif len(xy) == 1:\n",
    "            x = xy[0]\n",
    "            x = (x - self.x_mean) / self.x_std\n",
    "            return (x, )\n",
    "    def decodes(self, xy):\n",
    "        if len(xy) == 2:\n",
    "            x, y = xy\n",
    "            x = x * self.x_std + self.x_mean\n",
    "            y = y * self.y_std + self.y_mean\n",
    "            return (x, y)\n",
    "        elif len(xy) == 1:\n",
    "            x = xy[0]\n",
    "            x = x * self.x_std + self.x_mean\n",
    "            return (x, )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a, b = TSTensor([1., 2, 3]), TSTensor([4., 5, 6])\n",
    "mean, std = a.mean(), b.std()\n",
    "tuple_batch_tfm = TSStandardizeTuple(mean, std)\n",
    "a_tfmd, b_tfmd = tuple_batch_tfm((a, b))\n",
    "test_ne(a, a_tfmd)\n",
    "test_ne(b, b_tfmd)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSCatEncode(Transform):\n",
    "    \"Encodes a variable based on a categorical array\"\n",
    "    def __init__(self, a, sel_var):\n",
    "        a_key = np.unique(a)\n",
    "        a_val = np.arange(1, len(a_key) + 1)\n",
    "        self.o2i = dict(zip(a_key, a_val))\n",
    "        self.a_key = torch.from_numpy(a_key)\n",
    "        self.sel_var = sel_var\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        o_ = o[:, self.sel_var]\n",
    "        o_val = torch.zeros_like(o_)\n",
    "        o_in_a = torch.isin(o_, self.a_key.to(o.device))\n",
    "        o_val[o_in_a] = o_[o_in_a].cpu().apply_(self.o2i.get).to(o.device) # apply is not available for cuda!!\n",
    "        o[:, self.sel_var] = o_val\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[2, 2, 2,  ..., 2, 2, 2],\n",
       "        [2, 2, 2,  ..., 2, 2, 2],\n",
       "        [0, 0, 0,  ..., 0, 0, 0],\n",
       "        ...,\n",
       "        [0, 0, 0,  ..., 0, 0, 0],\n",
       "        [0, 0, 0,  ..., 0, 0, 0],\n",
       "        [4, 4, 4,  ..., 4, 4, 4]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# static input\n",
    "a = np.random.randint(10, 20, 512)[:, None, None].repeat(10, 1).repeat(28, 2)\n",
    "b = TSTensor(torch.randint(0, 30, (512,), device='cpu').unsqueeze(-1).unsqueeze(-1).repeat(1, 10, 28))\n",
    "output = TSCatEncode(a, sel_var=0)(b)\n",
    "test_eq(0 <= output[:, 0].min() <= len(np.unique(a)), True)\n",
    "test_eq(0 <= output[:, 0].max() <= len(np.unique(a)), True)\n",
    "test_eq(output[:, 0], output[:, 0, 0][:, None].repeat(1, 28))\n",
    "output[:, 0].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 6,  0,  0,  ...,  8,  7,  8],\n",
       "        [ 0,  8, 10,  ...,  0,  1,  0],\n",
       "        [ 0,  0,  0,  ...,  0,  0,  5],\n",
       "        ...,\n",
       "        [ 0,  0,  0,  ...,  0, 10,  3],\n",
       "        [ 6,  0,  0,  ...,  7,  9,  0],\n",
       "        [ 2,  0,  0,  ...,  2,  0,  0]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# non-static input\n",
    "a = np.random.randint(10, 20, 512)[:, None, None].repeat(10, 1).repeat(28, 2)\n",
    "b = TSTensor(torch.randint(0, 30, (512, 10, 28), device='cpu'))\n",
    "output = TSCatEncode(a, sel_var=0)(b)\n",
    "test_eq(0 <= output[:, 0].min() <= len(np.unique(a)), True)\n",
    "test_eq(0 <= output[:, 0].max() <= len(np.unique(a)), True)\n",
    "test_ne(output[:, 0], output[:, 0, 0][:, None].repeat(1, 28))\n",
    "output[:, 0].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSDropFeatByKey(Transform):\n",
    "    \"\"\"Randomly drops selected features at selected steps based\n",
    "    with a given probability per feature, step and a key variable\"\"\"\n",
    "    parameters, order = 'p', 90\n",
    "\n",
    "    def __init__(self,\n",
    "    key_var, # int representing the variable that contains the key information\n",
    "    p, # array of shape (n_keys, n_features, n_steps) representing the probabilities of dropping a feature at a given step for a given key\n",
    "    sel_vars, # int or slice or list of ints or array of ints representing the variables to drop\n",
    "    sel_steps=None, # int or slice or list of ints or array of ints representing the steps to drop\n",
    "    **kwargs,\n",
    "    ):\n",
    "        super().__init__(**kwargs)\n",
    "        if isinstance(p, np.ndarray):\n",
    "            p = torch.from_numpy(p)\n",
    "        if not isinstance(sel_vars, slice):\n",
    "            if isinstance(sel_vars, Integral): sel_vars = [sel_vars]\n",
    "            sel_vars = np.asarray(sel_vars)\n",
    "            if not isinstance(sel_steps, slice) and sel_steps is not None:\n",
    "                sel_vars = sel_vars.reshape(-1, 1)\n",
    "        if sel_steps is None:\n",
    "            sel_steps = slice(None)\n",
    "        elif not isinstance(sel_steps, slice):\n",
    "            if isinstance(sel_steps, Integral): sel_steps = [sel_steps]\n",
    "            sel_steps = np.asarray(sel_steps)\n",
    "            if not isinstance(sel_vars, slice):\n",
    "                sel_steps = sel_steps.reshape(1, -1)\n",
    "        self.key_var, self.p = key_var, p\n",
    "        self.sel_vars, self.sel_steps = sel_vars, sel_steps\n",
    "        if p.shape[-1] == 1:\n",
    "            if isinstance(self.sel_vars, slice) or isinstance(self.sel_steps, slice):\n",
    "                self._idxs = [slice(None), slice(None), slice(None), 0]\n",
    "            else:\n",
    "                self._idxs = [slice(None), 0, slice(None), slice(None), 0]\n",
    "        else:\n",
    "            if isinstance(self.sel_vars, slice) or isinstance(self.sel_steps, slice):\n",
    "                self._idxs = self._idxs = [slice(None), np.arange(p.shape[-1]), slice(None), np.arange(p.shape[-1])]\n",
    "            else:\n",
    "                self._idxs = [slice(None), 0, np.arange(p.shape[-1]), slice(None), np.arange(p.shape[-1])]\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        o_slice = o[:, self.sel_vars, self.sel_steps]\n",
    "        o_values = o[:, self.key_var, self.sel_steps]\n",
    "        o_values = torch.nan_to_num(o_values)\n",
    "        o_values = torch.round(o_values).long()\n",
    "        if self.p.shape[-1] == 1:\n",
    "            p = self.p[o_values][self._idxs].permute(0, 2, 1)\n",
    "        else:\n",
    "            p = self.p[o_values][self._idxs].permute(1, 2, 0)\n",
    "        mask = torch.rand_like(o_slice) < p\n",
    "        o_slice[mask] = np.nan\n",
    "        o[:, self.sel_vars, self.sel_steps] = o_slice\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_devices = 4\n",
    "key_var = 0\n",
    "\n",
    "for sel_vars in [1, [1], [1,3,5], slice(3, 5)]:\n",
    "    for sel_steps in [None, -1, 27, [27], [25, 26], slice(10, 20)]:\n",
    "        o = TSTensor(torch.rand(512, 10, 28))\n",
    "        o[:, key_var] = torch.randint(0, n_devices, (512, 28))\n",
    "        n_vars = 1 if isinstance(sel_vars, Integral) else len(sel_vars) if isinstance(sel_vars, list) else sel_vars.stop - sel_vars.start\n",
    "        n_steps = o.shape[-1] if sel_steps is None else 1 if isinstance(sel_steps, Integral) else \\\n",
    "            len(sel_steps) if isinstance(sel_steps, list) else sel_steps.stop - sel_steps.start\n",
    "        p = torch.rand(n_devices, n_vars, n_steps) * .5 + .5\n",
    "        output = TSDropFeatByKey(key_var, p, sel_vars, sel_steps)(o)\n",
    "        assert torch.isnan(output).sum((0, 2))[sel_vars].sum() > 0\n",
    "        assert torch.isnan(output).sum((0, 2))[~np.array(np.arange(o.shape[1])[sel_vars])].sum() == 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSClipOutliers(Transform):\n",
    "    \"Clip outliers batch of type `TSTensor` based on the IQR\"\n",
    "    parameters, order = L('min', 'max'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, min=None, max=None, by_sample=False, by_var=False, use_single_batch=False, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.min = tensor(min) if min is not None else tensor(-np.inf)\n",
    "        self.max = tensor(max) if max is not None else tensor(np.inf)\n",
    "        self.by_sample, self.by_var = by_sample, by_var\n",
    "        self._setup = (min is None or max is None) and not by_sample\n",
    "        if by_sample and by_var: self.axis = (2)\n",
    "        elif by_sample: self.axis = (1, 2)\n",
    "        elif by_var: self.axis = (0, 2)\n",
    "        else: self.axis = None\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.verbose = verbose\n",
    "        if min is not None or max is not None:\n",
    "            pv(f'{self.__class__.__name__} min={min}, max={max}\\n', self.verbose)\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "            min, max = get_outliers_IQR(o, self.axis)\n",
    "            self.min, self.max = tensor(min), tensor(max)\n",
    "            if self.axis is None: pv(f'{self.__class__.__name__} min={self.min}, max={self.max}, by_sample={self.by_sample}, by_var={self.by_var}\\n',\n",
    "                                     self.verbose)\n",
    "            else: pv(f'{self.__class__.__name__} min={self.min.shape}, max={self.max.shape}, by_sample={self.by_sample}, by_var={self.by_var}\\n',\n",
    "                     self.verbose)\n",
    "            self._setup = False\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.axis is None: return torch.clamp(o, self.min, self.max)\n",
    "        elif self.by_sample:\n",
    "            min, max = get_outliers_IQR(o, axis=self.axis)\n",
    "            self.min, self.max = o.new(min), o.new(max)\n",
    "        return torch_clamp(o, self.min, self.max)\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(by_sample={self.by_sample}, by_var={self.by_var})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TSClipOutliers min=-1, max=1\n",
      "\n"
     ]
    }
   ],
   "source": [
    "batch_tfms=[TSClipOutliers(-1, 1, verbose=True)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb.max() <= 1\n",
    "assert xb.min() >= -1\n",
    "test_close(xb.min(), -1, eps=1e-1)\n",
    "test_close(xb.max(), 1, eps=1e-1)\n",
    "xb, yb = next(iter(dls.valid))\n",
    "test_close(xb.min(), -1, eps=1e-1)\n",
    "test_close(xb.max(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSClip(Transform):\n",
    "    \"Clip  batch of type `TSTensor`\"\n",
    "    parameters, order = L('min', 'max'), 90\n",
    "    def __init__(self, min=-6, max=6, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.min = torch.tensor(min)\n",
    "        self.max = torch.tensor(max)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch.clamp(o, self.min, self.max)\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(min={self.min}, max={self.max})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.randn(10, 20, 100)*10)\n",
    "test_le(TSClip()(t).max().item(), 6)\n",
    "test_ge(TSClip()(t).min().item(), -6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSSelfMissingness(Transform):\n",
    "    \"Applies missingness from samples in a batch to random samples in the batch for selected variables\"\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, **kwargs):\n",
    "        self.sel_vars = sel_vars\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.sel_vars is not None:\n",
    "            mask = rotate_axis0(torch.isnan(o[:, self.sel_vars]))\n",
    "            o[:, self.sel_vars] = o[:, self.sel_vars].masked_fill(mask, np.nan)\n",
    "        else:\n",
    "            mask = rotate_axis0(torch.isnan(o))\n",
    "            o.masked_fill_(mask, np.nan)\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.randn(10, 20, 100))\n",
    "t[t>.8] = np.nan\n",
    "t2 = TSSelfMissingness()(t.clone())\n",
    "t3 = TSSelfMissingness(sel_vars=[0,3,5,7])(t.clone())\n",
    "assert (torch.isnan(t).sum() < torch.isnan(t2).sum()) and (torch.isnan(t2).sum() >  torch.isnan(t3).sum())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSRobustScale(Transform):\n",
    "    r\"\"\"This Scaler removes the median and scales the data according to the quantile range (defaults to IQR: Interquartile Range)\"\"\"\n",
    "    parameters, order = L('median', 'iqr'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, median=None, iqr=None, quantile_range=(25.0, 75.0), use_single_batch=True, exc_vars=None, eps=1e-8, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.median = tensor(median) if median is not None else None\n",
    "        self.iqr = tensor(iqr) if iqr is not None else None\n",
    "        self._setup = median is None or iqr is None\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.exc_vars = exc_vars\n",
    "        self.eps = eps\n",
    "        self.verbose = verbose\n",
    "        self.quantile_range = quantile_range\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "\n",
    "            new_o = o.permute(1,0,2).flatten(1)\n",
    "            median = get_percentile(new_o, 50, axis=1)\n",
    "            iqrmin, iqrmax = get_outliers_IQR(new_o, axis=1, quantile_range=self.quantile_range)\n",
    "            self.median = median.unsqueeze(0)\n",
    "            self.iqr = torch.clamp_min((iqrmax - iqrmin).unsqueeze(0), self.eps)\n",
    "            if self.exc_vars is not None:\n",
    "                self.median[:, self.exc_vars] = 0\n",
    "                self.iqr[:, self.exc_vars] = 1\n",
    "\n",
    "            pv(f'{self.__class__.__name__} median={self.median.shape} iqr={self.iqr.shape}', self.verbose)\n",
    "            self._setup = False\n",
    "        else:\n",
    "            if self.median is None: self.median = torch.zeros(1, device=dl.device)\n",
    "            if self.iqr is None: self.iqr = torch.ones(1, device=dl.device)\n",
    "\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return (o - self.median) / self.iqr\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(quantile_range={self.quantile_range}, use_single_batch={self.use_single_batch})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TSRobustScale median=torch.Size([1, 24, 1]) iqr=torch.Size([1, 24, 1])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "TSTensor([-2.2415947914123535], device=mps:0, dtype=torch.float32)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "batch_tfms = TSRobustScale(verbose=True, use_single_batch=False)\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, batch_tfms=batch_tfms, num_workers=0)\n",
    "xb, yb = next(iter(dls.train))\n",
    "xb.min()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([ 0.0000, -1.7305,  0.0000,  0.7365, -1.2736, -0.5528,  0.0000, -0.7074,\n",
      "         0.0000,  0.7087, -0.7014, -0.1120,  0.0000, -1.3332, -0.5958,  0.7563,\n",
      "        -1.0129, -0.3985, -0.5186, -1.5125, -0.7353,  0.7326, -1.1495, -0.5359],\n",
      "       device='mps:0')\n",
      "tensor([1.0000, 4.2788, 1.0000, 4.8008, 8.0682, 2.2777, 1.0000, 0.6955, 1.0000,\n",
      "        1.4875, 2.6386, 1.4756, 1.0000, 2.9811, 1.2507, 3.2291, 5.9906, 1.9098,\n",
      "        1.3428, 3.6368, 1.3689, 4.4213, 6.9907, 2.1939], device='mps:0')\n"
     ]
    }
   ],
   "source": [
    "exc_vars = [0, 2, 6, 8, 12]\n",
    "batch_tfms = TSRobustScale(use_single_batch=False, exc_vars=exc_vars)\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, batch_tfms=batch_tfms, num_workers=0)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_eq(len(dls.train.after_batch.fs[0].median.flatten()), 24)\n",
    "test_eq(len(dls.train.after_batch.fs[0].iqr.flatten()), 24)\n",
    "test_eq(dls.train.after_batch.fs[0].median.flatten()[exc_vars].cpu(), torch.zeros(len(exc_vars)))\n",
    "test_eq(dls.train.after_batch.fs[0].iqr.flatten()[exc_vars].cpu(), torch.ones(len(exc_vars)))\n",
    "print(dls.train.after_batch.fs[0].median.flatten().data)\n",
    "print(dls.train.after_batch.fs[0].iqr.flatten().data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "def get_stats_with_uncertainty(o, sel_vars=None, sel_vars_zero_mean_unit_var=False, bs=64, n_trials=None, axis=(0,2)):\n",
    "    o_dtype = o.dtype\n",
    "    if n_trials is None: n_trials = len(o) // bs\n",
    "    random_idxs = random_choice(len(o), n_trials * bs, n_trials * bs > len(o))\n",
    "    oi_mean = []\n",
    "    oi_std = []\n",
    "    start = 0\n",
    "    for i in progress_bar(range(n_trials)):\n",
    "        idxs = random_idxs[start:start + bs]\n",
    "        start += bs\n",
    "        if hasattr(o, 'oindex'):\n",
    "            oi = o.index[idxs]\n",
    "        if hasattr(o, 'compute'):\n",
    "            oi = o[idxs].compute()\n",
    "        else:\n",
    "            oi = o[idxs]\n",
    "        oi_mean.append(np.nanmean(oi.astype('float32'), axis=axis, keepdims=True))\n",
    "        oi_std.append(np.nanstd(oi.astype('float32'), axis=axis, keepdims=True))\n",
    "    oi_mean = np.concatenate(oi_mean)\n",
    "    oi_std = np.concatenate(oi_std)\n",
    "    E_mean = np.nanmean(oi_mean, axis=0, keepdims=True).astype(o_dtype)\n",
    "    S_mean = np.nanstd(oi_mean, axis=0, keepdims=True).astype(o_dtype)\n",
    "    E_std = np.nanmean(oi_std, axis=0, keepdims=True).astype(o_dtype)\n",
    "    S_std = np.nanstd(oi_std, axis=0, keepdims=True).astype(o_dtype)\n",
    "    if sel_vars is not None:\n",
    "        non_sel_vars = np.isin(np.arange(o.shape[1]), sel_vars, invert=True)\n",
    "        if sel_vars_zero_mean_unit_var:\n",
    "            E_mean[:, non_sel_vars] = 0 # zero mean\n",
    "            E_std[:, non_sel_vars] = 1  # unit var\n",
    "        S_mean[:, non_sel_vars] = 0 # no uncertainty\n",
    "        S_std[:, non_sel_vars] = 0  # no uncertainty\n",
    "    return np.stack([E_mean, S_mean, E_std, S_std])\n",
    "\n",
    "\n",
    "def get_random_stats(E_mean, S_mean, E_std, S_std):\n",
    "    mult = np.random.normal(0, 1, 2)\n",
    "    new_mean = E_mean + S_mean * mult[0]\n",
    "    new_std = E_std + S_std * mult[1]\n",
    "    return new_mean, new_std\n",
    "\n",
    "\n",
    "class TSGaussianStandardize(Transform):\n",
    "    \"Scales each batch using modeled mean and std based on UNCERTAINTY MODELING FOR OUT-OF-DISTRIBUTION GENERALIZATION https://arxiv.org/abs/2202.03958\"\n",
    "\n",
    "    parameters, order = L('E_mean', 'S_mean', 'E_std', 'S_std'), 90\n",
    "    def __init__(self,\n",
    "        E_mean : np.ndarray, # Mean expected value\n",
    "        S_mean : np.ndarray, # Uncertainty (standard deviation) of the mean\n",
    "        E_std : np.ndarray,  # Standard deviation expected value\n",
    "        S_std : np.ndarray,  # Uncertainty (standard deviation) of the standard deviation\n",
    "        eps=1e-8, # (epsilon) small amount added to standard deviation to avoid deviding by zero\n",
    "        split_idx=0, # Flag to indicate to which set is this transofrm applied. 0: training, 1:validation, None:both\n",
    "        **kwargs,\n",
    "        ):\n",
    "        self.E_mean, self.S_mean = torch.from_numpy(E_mean), torch.from_numpy(S_mean)\n",
    "        self.E_std, self.S_std = torch.from_numpy(E_std), torch.from_numpy(S_std)\n",
    "        self.eps = eps\n",
    "        super().__init__(split_idx=split_idx, **kwargs)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        mult = torch.normal(0, 1, (2,), device=o.device)\n",
    "        new_mean = self.E_mean + self.S_mean * mult[0]\n",
    "        new_std = torch.clamp(self.E_std + self.S_std * mult[1], self.eps)\n",
    "        return (o - new_mean) / new_std\n",
    "\n",
    "TSRandomStandardize = TSGaussianStandardize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<style>\n",
       "    /* Turns off some styling */\n",
       "    progress {\n",
       "        /* gets rid of default border in Firefox and Opera. */\n",
       "        border: none;\n",
       "        /* Needs to be in here for Safari polyfill so background images work as expected. */\n",
       "        background-size: auto;\n",
       "    }\n",
       "    progress:not([value]), progress:not([value])::-webkit-progress-bar {\n",
       "        background: repeating-linear-gradient(45deg, #7e7e7e, #7e7e7e 10px, #5c5c5c 10px, #5c5c5c 20px);\n",
       "    }\n",
       "    .progress-bar-interrupted, .progress-bar-interrupted::-webkit-progress-bar {\n",
       "        background: #F44336;\n",
       "    }\n",
       "</style>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    <div>\n",
       "      <progress value='15' class='' max='15' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
       "      100.00% [15/15 00:00&lt;00:00]\n",
       "    </div>\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "(array([[[0.50965549],\n",
       "         [0.51006714]]]),\n",
       " array([[[0.28785178],\n",
       "         [0.28835465]]]))"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr = np.random.rand(1000, 2, 50)\n",
    "E_mean, S_mean, E_std, S_std = get_stats_with_uncertainty(arr, sel_vars=None, bs=64, n_trials=None, axis=(0,2))\n",
    "new_mean, new_std = get_random_stats(E_mean, S_mean, E_std, S_std)\n",
    "new_mean2, new_std2 = get_random_stats(E_mean, S_mean, E_std, S_std)\n",
    "test_ne(new_mean, new_mean2)\n",
    "test_ne(new_std, new_std2)\n",
    "test_eq(new_mean.shape, (1, 2, 1))\n",
    "test_eq(new_std.shape, (1, 2, 1))\n",
    "new_mean, new_std"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "TSGaussianStandardize can be used jointly with TSStandardized in the following way: \n",
    "\n",
    "```python\n",
    "X, y, splits = get_UCR_data('LSST', split_data=False)\n",
    "tfms = [None, TSClassification()]\n",
    "E_mean, S_mean, E_std, S_std = get_stats_with_uncertainty(X, sel_vars=None, bs=64, n_trials=None, axis=(0,2))\n",
    "batch_tfms = [TSGaussianStandardize(E_mean, S_mean, E_std, S_std, split_idx=0), TSStandardize(E_mean, S_mean, split_idx=1)]\n",
    "dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=[32, 64])\n",
    "learn = ts_learner(dls, InceptionTimePlus, metrics=accuracy, cbs=[ShowGraph()])\n",
    "learn.fit_one_cycle(1, 1e-2)\n",
    "```\n",
    "In this way the train batches are scaled based on mean and standard deviation distributions while the valid batches are scaled with a fixed mean and standard deviation values.\n",
    "\n",
    "The intent is to improve out-of-distribution performance. This method is inspired by UNCERTAINTY MODELING FOR OUT-OF-DISTRIBUTION GENERALIZATION https://arxiv.org/abs/2202.03958."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSDiff(Transform):\n",
    "    \"Differences batch of type `TSTensor`\"\n",
    "    order = 90\n",
    "    def __init__(self, lag=1, pad=True, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.lag, self.pad = lag, pad\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch_diff(o, lag=self.lag, pad=self.pad)\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(lag={self.lag}, pad={self.pad})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.arange(24).reshape(2,3,4))\n",
    "test_eq(TSDiff()(t)[..., 1:].float().mean(), 1)\n",
    "test_eq(TSDiff(lag=2, pad=False)(t).float().mean(), 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSLog(Transform):\n",
    "    \"Log transforms batch of type `TSTensor` + 1. Accepts positive and negative numbers\"\n",
    "    order = 90\n",
    "    def __init__(self, ex=None, **kwargs):\n",
    "        self.ex = ex\n",
    "        super().__init__(**kwargs)\n",
    "    def encodes(self, o:TSTensor):\n",
    "        output = torch.zeros_like(o)\n",
    "        output[o > 0] = torch.log1p(o[o > 0])\n",
    "        output[o < 0] = -torch.log1p(torch.abs(o[o < 0]))\n",
    "        if self.ex is not None: output[...,self.ex,:] = o[...,self.ex,:]\n",
    "        return output\n",
    "    def decodes(self, o:TSTensor):\n",
    "        output = torch.zeros_like(o)\n",
    "        output[o > 0] = torch.exp(o[o > 0]) - 1\n",
    "        output[o < 0] = -torch.exp(torch.abs(o[o < 0])) + 1\n",
    "        if self.ex is not None: output[...,self.ex,:] = o[...,self.ex,:]\n",
    "        return output\n",
    "    def __repr__(self): return f'{self.__class__.__name__}()'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.rand(2,3,4)) * 2 - 1\n",
    "tfm = TSLog()\n",
    "enc_t = tfm(t)\n",
    "test_ne(enc_t, t)\n",
    "test_close(tfm.decodes(enc_t).data, t.data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSCyclicalPosition(Transform):\n",
    "    \"Concatenates the position along the sequence as 2 additional variables (sine and cosine)\"\n",
    "    order = 90\n",
    "    def __init__(self,\n",
    "        cyclical_var=None, # Optional variable to indicate the steps withing the cycle (ie minute of the day)\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        drop_var=False, # Flag to indicate if the cyclical var is removed\n",
    "        **kwargs\n",
    "        ):\n",
    "        super().__init__(**kwargs)\n",
    "        self.cyclical_var, self.drop_var = cyclical_var, drop_var\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs,nvars,seq_len = o.shape\n",
    "        if self.cyclical_var is None:\n",
    "            sin, cos = sincos_encoding(seq_len, device=o.device)\n",
    "            output = torch.cat([o, sin.reshape(1,1,-1).repeat(bs,1,1), cos.reshape(1,1,-1).repeat(bs,1,1)], 1)\n",
    "            return output\n",
    "        else:\n",
    "            sin = torch.sin(o[:, [self.cyclical_var]]/seq_len * 2 * np.pi)\n",
    "            cos = torch.cos(o[:, [self.cyclical_var]]/seq_len * 2 * np.pi)\n",
    "            if self.drop_var:\n",
    "                exc_vars = np.isin(np.arange(nvars), self.cyclical_var, invert=True)\n",
    "                output = torch.cat([o[:, exc_vars], sin, cos], 1)\n",
    "            else:\n",
    "                output = torch.cat([o, sin, cos], 1)\n",
    "            return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAGdCAYAAAAfTAk2AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAetZJREFUeJzt3Qd4VNXWBuCP9F5JJQkh9BAgECAEEFGQKogVFEUQ4Ypiw2vhXoVrxfbbESkioAiICiIqgvQSCIQaegmkJ0BI75n5n71PEohSwpDJmTPzvc9zyJ6SycqQZNbss/dajfR6vR5EREREZsRK7QCIiIiI6hsTHCIiIjI7THCIiIjI7DDBISIiIrPDBIeIiIjMDhMcIiIiMjtMcIiIiMjsMMEhIiIis2MDC6TT6ZCWlgZXV1c0atRI7XCIiIioDkRt4vz8fAQGBsLK6tpzNBaZ4IjkJjg4WO0wiIiIyADJyckICgq65n0sMsERMzfVT5Cbm5va4RAREVEd5OXlyQmK6tfxa7HIBKf6tJRIbpjgEBERaUtdlpdwkTERERGZHSY4REREZHaY4BAREZHZYYJDREREZocJDhEREZkdJjhERERkdpjgEBERkdlhgkNERERmhwkOERERmR2jJjibN2/G0KFDZVMsUXVwxYoV1/2cjRs3onPnzrC3t0eLFi0wf/78f9xnxowZCA0NhYODA6KjoxEXF2ek74CIiIi0yKgJTmFhITp27CgTkrpITEzEkCFDcNttt2Hfvn147rnn8Pjjj+PPP/+suc/SpUsxefJkTJs2DXv27JGPP2DAAGRlZRnxOyEiIiItaaQXvccb4gs1aoTly5dj+PDhV73Pyy+/jN9++w0JCQk1140cORI5OTlYvXq1vCxmbLp27YovvvhCXtbpdLLx1tNPP41XXnmlzs263N3dkZuby15UREREGnEjr98m1WwzNjYW/fr1q3WdmJ0RMzlCWVkZ4uPjMWXKlJrbrays5OeIz72a0tJSeVz+BBlF8i7gwBLA0Qtw8gIcPZWxWwDg0xawNqmnm2SCrMfp84VIzSlGTlEZLhaWIae4HAUlFXC2t4Gnky08ne3g7miLIE9HhDV2gZXV9Zu8ERFZrLJCIG0v4OwD+LRWLQyTesXNyMiAn59frevEZZGQFBcX4+LFi6isrLzifY4ePXrVx50+fTpef/11GF3GAWDX3CvfZusEBHYCmkQBQV2BZrcoCRA1qOKySuxIvIC9STnYm3QR+5JzkF9SUefPd3WwQWSwBzqJI8QT3cO84WhnbdSYiYhM2sWzwJmtQMouIGU3kHUI0OuAmEnAgLdVC8ukEhxjETM+Yt1ONZEwidNa9S4gEuj9IlCUDRRfBIqzlbH4zy/NBc5uUw7B2g5oNRDoMAJo2R+wsav/eEiq1Omx8/QF/Lw3FasTMlBQWjuhcbC1Qqi3Mzyd7ODpbAt3RzuZyIj7iVmdnKJyXCwqx5nzhTIZ2nLivDwEZztrDIwIwD2dm8hkx5qzO0RkCYqygUPLgQM/AMk7/nm7WxPA1hFqMqkEx9/fH5mZmbWuE5fFeTZHR0dYW1vL40r3EZ97NWJHljiMLkjMzkT983qdDrhwQslsRYZ7djtw/hhwZKVyiJmciHuVbNermfHjtBDnC0rxzbZE/BSfioy8kprrm3g4ymQkMkSZiWnt7wpb6+uvt6+o1OFYZn7V7E8Odpy+IE9t/bQnRR7+bg64u3MTPNazGXxcG+DnjYiooZ3ZBuz4EjixBqgsU65rZAUEdQOCuylnKIK6AG6BakdqWglOTEwMfv/991rXrV27Vl4v2NnZISoqCuvWratZrCwWGYvLkyZNgsmyslLOQ4qj0yjluowEZb3OwR+B/HTl1Fb8fKDjg8oskGdTtaPWrOzCMszafAoLt59FcXmlvM7NwQZDOgTKmZYuTT3lovcbZWNthXaB7vJ4uHtTiPX58Wcvypmh3w6kyyRq5sZTmL/tDEbHNMWE3mHwdmGiQ0Rm4GwssPEdIHHzpev82gMdRwAR9ylrTS1pF1VBQQFOnjwpx506dcJHH30kt4B7eXkhJCREnjpKTU3FwoULa7aJR0RE4KmnnsJjjz2G9evX45lnnpE7q8Ri4+pt4o8++ihmzZqFbt264ZNPPsEPP/wg1+D8fW2OJnZR6SqBxE1A7Azg5F/KdVY2QKeHlUTHPUjd+DQkt6hcJjYLtp9BYZmS2HQIcscTtzZH37a+sLcx3lqZ0opKbDiahZmbTmN/co68zsnOGqNjQvHErWHwcOIpSCLSoJTdwPq3gNMblMtWtsrrU7fxgF+7Bg/nRl6/jZrgiKJ9IqH5O5GgiAJ+Y8aMwZkzZ+T9Lv+c559/HocPH0ZQUBBee+01eb/LiS3iH3zwgVyUHBkZic8++0xuH68rk0pwLpccB2x459IPkq0z0G8a0HW8MgtEVyR+hH8/mIFpKxNwvkCZMm0X6IbJd7TC7W18DZqtuZlYNhzLwsdrT+Bgaq68ztvZDv8b1g53dgho0FiIiAxWkgf8NQ3YPe/SG+/IUUDvfwMeIVCLySQ4pspkE5zLpwL/+t+lhVvi3OawzwHfNmpHZnIy80rw6ooErD2srMtq7uOMlwe2wR3hfqomE+LX6q8jWXh/9VGcyCqQ1/Vr64s3h0cgwF3dhXdERNd0bDXw22QgL1W53PEhoM/LgGco1MYER+sJTvXC5Ph5wNr/AWX5yq4rccqq1/OAtS0snfixXborGW//fkTubLKxaoQnb2uBp25rbtRTUTeqrEKHLzeexIwNJ1FeqYervQ1eGdwGD3UL4WwOEZmWwvPAHy8BCT8pl73CgKGfKWVNTAQTHHNIcKrlpgCrJgMnqtpVNO0J3PcN4Fq39UbmqLC0Ai//dACrDqTLyx2D3PHefR3Qxt90/y+PZ+bjpR8PyLo7wpD2ATJmF3uTWudPRJa81uaH0cqsjdgV1eNpoM8U1bd6/x0THHNKcATxX3RwmZLoiNkc1wDg/gVASN3XHZmL0+cK8MR38TieWSBnbV4c0BqP3xKmifozoh6P2Lb+3uqjcjanha8LZj0SheY+LmqHRkSWSq8H4r8B/nhZ2fbt3RK4ZzbQpDNMERMcc0twqp0/ASx9GDh3VFnJPnA60PVx0egLlmDNoQy88MN+5JdWwNfVHl+O6owuoV7QGrG1/MlF8cjMK5UzOB/e30EWCyQialDlxcBvLwD7FimX2w4F7voScDDd10UmOOaa4AilBcAvTwGHV1xa/DX0U7OuhCx+RD/+6wQ+W3dCXu4a6okZD3WGr5sDtCorvwSTvt+LuMRseXnSbS3wQv9WXJdDRA0jLx1YPAJI36+ckuo7Fej5nMm/Yb6R12/uPdYaexfg/vlA/7eBRtbA/u+BJQ8qzc3MkDit85/lCTXJzdieofh+fHdNJzeCr6sDFj0ejcd7KZWrv9hwEq/8dFBWSyYiMqoLp4B5/ZXkxskbeGS5soHFxJObG8UER4vED2GPScBDPwA2jkqBwIXDld4gZkQUz3t68R4sjkuS3/Lbd0dg2tB2dWqroAXi+3j1znC8e097iCVES3cn46nv96CkqvoyEVG9S98PzBsA5CQBns2A8euBsD4wR+bxSmGpWvYDHl0JOHgAKXHAN4OBvDSYA9Ho8rH5u2QBPztrK3lKalS0ebavGNktRK4nEt/nn4cyMfabXcgvKVc7LCIyxz5S8+8ECs8pbRbGrTGJ2jbGwgRH60Rzs7F/AC7+wLkjwNcDlOlHDbtYWIZRc3Zg28kLst3BvDFdMbi9eS/CFYuM54/tKruTx56+gIfm7JQ9tYiI6q1433f3AKV5SrmRsb8BLr4wZ0xwzIFfODDuT6UoU24SsGAYkJMMLRIzF49+E4f9KbnwdLLF4vHd0atlY1iCHi0aY8mEGHg528k2D6Pn7UQeZ3KI6Gad/EvZgVtRArQeDDz8E+DgDnPHBMdciGnGsasB7xZAXgqwcBiQr7Qv0IriskqMm78bB1Jy5Yv80n/FoGOwByxJ+yB3/PCv7rJ/VUJqnjxdVVRWoXZYRKTl01JLHgZ05UD4XcAD35pc8T5jYYJjTkR149G/AO4hQPZp4FvtLDwWC4onfLsbcWey4epgg4WPdUMrP1dYoha+rvh2XDTcHGxkzZzxC3dz4TER3bjUeOD7EUBFMdByAHDPXMDacqqnM8ExN+5BwKO/KGtysg4r51xFV1gTVl6pw9Pf78WWE+flmhuxFiWiiflPn15LeKAb5j/WTa7JEWuRJn2/Rz5PRER1knkI+PYepfp96C3AAwvMul7alTDBMUdiLY6YyRH1DdL2Khl8eQlMtYjfyz8ewJrDmbCzscKc0V0Q1VR71YmNoXOIJ+Y+2hX2NlayM7mo4qzTWVxdTiK6UWIGf+FwoCQHCOoKPLjYYk5LXY4JjrnybQM8/DNg7w4kbVeqH5tg0epP/jqBn/emyr5SM0d1Rs8WlrGguK5imnvjq0eiYGvdCCv3p+GjtcfVDomITFlxjvKmtjBL2Qo+ahlgb5mn+5ngmLPASGDkd4CVDZDwI7DpPZiSFXtT8WlVheJ37m6Pvm0tt0P6tdzW2hfv3tOhpuLxT/EpaodERKaoslzpCH7+OOAWBDz8I+DoCUvFBMfcNesN3PmxMt44HTiwDKZg95lsvPTjATn+161heKBrsNohmbR7o4Lw1G3N5fiVnw9g5+kLaodERKZEzNCLxpmJmwA7F+ChJYCrPywZExxL0Hk00OMZZfzLk0DSTlXDSbpQhAnfxqOsUocB7fzw8oA2qsajFS/c0RqD2/ujvFKPf30XjzPnzbP/GBEZIPYLYM8CpXHmvV8D/u1h6ZjgWIp+rwNt7gQqy4AlDwEXz6gShihc99iCXbJKb0QTN3w8IhJWohETXZd4nv7v/kh0DHJHTlG5bGWRW8RCgEQW7+jvwJrXlLFoxNx6oNoRmQQmOJbCygq4ZzYQ0BEoOq8UfiovbtAQxA6gyUv34WRWAfzdHPD1o13hZGc5NRnqg6OdNeY82gWB7g44fb4Qzy7dy51VRJbs3HHg5/HiHBXQ5TGg+0S1IzIZTHAsiZ0z8OASwNkHyDwI/P7vBv3yszafltudq7eD+7k5NOjXNxe+rg4yyRHbxzceO4cvN55UOyQiUkNZobKouKwAaNoLGPQ+0Igz4tWY4Fgat0Dl/Kw4T7v3O2DPtw3yZWNPXcAHfx6V49eHtZMtCchw7QLd8ebwCDkWW8e3nTyvdkhE1NCLilc9rzRZFoVd75sHWNuqHZVJYYJjicJuBW77rzIWszjpym4mY8nKK8HTi/dCnEm5t3MQRnLHVL14oEswHugSJJ/XZxbvRUauaRZzJCIj2D0POLAUaGStJDeiVQ/VwgTHUvWarPQmEd1lxRSnKA5lBKK9wKTv9+J8QSna+LvireERaMQp1Hrzxl0RaBvghguFZWznQGQpUvcAq19Rxv2mAaE91Y7IJDHBseRFx3d/BXiEABcTjVbp+MM/j8kGmi72NvhyVGe5SJbqj4OttawA7Wpvg91nL+LdP5TTgERkpkQD5R8eVXbEip2x1SVA6B+Y4FgyJy/g/gWAtR1wdBWwa269Pvym4+fkwmLhg/s6IMzHpV4fnxShjZ3x4QMd5fjrrYnYcDRL7ZCIyBjEm9BfnwFykwDPZsBdM7io+BqY4Fi6Jp2B/m8p4zWvAueO1cvDijo3/162X45HxzTFoPYB9fK4dGUD2vljbM9QOX7xx/3ylCARmZl9i4Ajvyrtd+7/BnD0UDsik8YEh4BuE4DmfZX1OD89DlSU3XSH8Fd+OoBz+aVo4euC/wxuW2+h0tW9PLANWvm54HxBGV756aD8fyAiM+oQ/sfLylhsEgnspHZEJo8JDilTnMO/BBy9gIwDwIa3b+rhlu1OwZrDmbID9icjIuU6ETI+8Tx/MqIT7Kyt8NeRTCyOS1Y7JCKqD5UVwM//qqp30xPo+azaEWkCExxSiKZswz5Txts+Bc5sNehhzl4oxP9+PSTHL/RvjYgmrHfTkMID3fDigNZy/Oaqwzh9rkDtkIjoZm39CEiJA+zdlM0hVnzTWBdMcOiStkOBTo8oJb+XP3HDW8crKnV4buk+FJVVIrqZF8bfEma0UOnqxvVqhpgwbxSXV+L5pfu4dZxIy1LigY3vKuMh/6fsfCXTSXBmzJiB0NBQODg4IDo6GnFxcVe9b58+fWSdlL8fQ4YMqbnPmDFj/nH7wIFsLlYvBr6rrM7PTQb+eOmGPnXmxlPYm5QDVwcbfDQiEtZsoqleU84HOsLNwQb7U3Lx+Xq2ciDSpLIipc+UvhKIuBdof7/aEWmK0ROcpUuXYvLkyZg2bRr27NmDjh07YsCAAcjKuvJW1p9//hnp6ek1R0JCAqytrXH//bX/Y0VCc/n9Fi9ebOxvxTLYuwD3zFFaOYgqmcf/rNOnHcvIx2frT8jxG3e1QxMPRyMHStcS6OGIt+5uL8dfbjiJw2l5aodERDdKrIfMPgW4BiqzN9wSbloJzkcffYTx48dj7NixCA8Px1dffQUnJyfMmzfvivf38vKCv79/zbF27Vp5/78nOPb29rXu5+npaexvxXIEdwVinlLGvz4HlORe8+6VOj1e+ukAyiv16NfWD8MjmzRMnHRNQzsEYEA7P1To9Hj5pwPyFCIRaejU1I4vlfHQTwBHvsaZVIJTVlaG+Ph49OvX79IXtLKSl2NjY+v0GF9//TVGjhwJZ2fnWtdv3LgRvr6+aN26NSZOnIgLFy5c9TFKS0uRl5dX66Dr6PMfwCsMyE8D1k695l3nbU3E/mTl1NTbd7MVg6kQ/w9v3hUhT1UdTM3FnC2JaodERHVRUVpVXV4HdBgBtBqgdkSaZNQE5/z586isrISfX+0mYOJyRkbGdT9frNURp6gef/zxf5yeWrhwIdatW4f33nsPmzZtwqBBg+TXupLp06fD3d295ggOZrPH67JzAoZ9oYzj5wOnN13xbonnC/HhGqU44KtD2sLPzaEho6Tr8HVzwGt3hsvxx38dxynuqiIyfVv+T+kS7uyjrIsk89tFJWZv2rdvj27dutW6XszoDBs2TN42fPhwrFq1Crt27ZKzOlcyZcoU5Obm1hzJyawPUieigVuXccp45dNAWWGtm3VVpz5KK3To1aKx7G5Npue+qCD0buWDsgodXv7xgPx/IyITlXFQSXCEwR8oLXXI9BKcxo0bywXCmZmZta4Xl8W6mWspLCzEkiVLMG5c1QvsNYSFhcmvdfLklXeLiPU6bm5utQ6qo37/A9yCgJyzwPqqlg5VFsUlIS4xG4621ph+T3uemjJR4v/lnbsj4GxnLRtyLow9o3ZIRHS1gn6/TAJ0FUojzfDhakekaUZNcOzs7BAVFSVPJVXT6XTyckxMzDU/d9myZXLtzMMPP3zdr5OSkiLX4AQEsN9RvXNwA4Z+qox3zARSdsthem4x3v39iBy/NLA1gr2c1IySriPI0wmvDGojx+//eQwpF4vUDomI/m7HDCB9H+Dgzl1TWjhFJbaIz5kzBwsWLMCRI0fkgmAxOyN2VQmjR4+Wp5CudHpKnH7y9vaudX1BQQFefPFF7NixA2fOnJHJ0l133YUWLVrI7edkBC37AR1GKgUAVz0v32WIKrmFZZXoHOKB0TFKk0cybaOim6JbqJcsxPjGr4fVDoeILpeTfKmg34B3lOrydFNsYGQjRozAuXPnMHXqVLmwODIyEqtXr65ZeJyUlCR3Vl3u2LFj2Lp1K9asWfOPxxOnvA4cOCATppycHAQGBqJ///5488035akoMhLRcfz4H7JX1YnfP8HvB9vKQn5v392eBf00VADwzeERGPLZFtkrbN2RTPRtW3sDABGpZPUrQHkRENIDiByldjRmoZHeAlsOi23iYjeVWHDM9Tg3YNfXwG+TUQAn3FbyAYb16lyzQ4e0Y/rvRzBr82kEeTpi7fO3wtGOfW2IVCUKqn7/AGBlA/xrC+DHv6v18fpt0ruoyMREjUG6Szu4oAhvOS3G83e0UjsiMsAzfVsi0N0BKReL8eVGtnEgUr0dw+//Vsbdn2RyU4+Y4FCdJWaX4MmcUajUN8IA3Va4pBrWcZzU5Wxvg6lD28nxrE2nWRuHSO1O4TlJgFsT4NaX1Y7GrDDBoToRZzKnrTyEvRWhWOc6TLnyt38rFTdJc0QLh9ta+6CsUoepvyTI/18iamDnTwDbqnapioJ+ohcg1RsmOFQnvx/MwObj52BnY4XWD74LOPsCF04A2z9TOzQysDbO68MiYG9jhW0nL2Dl/jS1QyKyLOJNhTg1VVkGtLgDaDtU7YjMDhMcuq7iskq89ZuyrXjirc3RtEmgso1R2Px/QG6KugGSQUK8nTDpthZyPP33oygqq1A7JCLLcWQlcHojYOOgVCxmzZt6xwSHrmvW5lNIzy1BEw9HTOzTXLmy/X3KdsaKYuCv/6kdIhlofO8wBHs5IiOvBF9tPKV2OESWobwEWPOqMu7xDODVTO2IzBITHLqmtJxifLVJeeH7z+C2cLCt2lIs3m0MnC4GwMFlQNJOdQMlg4j/z/8ObivHYus4KxwTNYDYL5SFxa6BQK/n1I7GbDHBoWt694+jKCnXyQq4g9v/rbJmYCTQqaqVxuqXRR8OVWKkmzOgnT+6h3nJpqnT/ziqdjhE5i0vHdjykTK+43XAzlntiMwWExy6qviz2XLxqZismTo0/MrNNPtOBexcgbS9wIElaoRJN0n8v069sx1EQerfDqTLBqpEZCTr3gDKC4GgrkD7+9WOxqwxwaEr0un0eL2qX9GILsGIaOJ+5Tu6+AK3vqiM/3odKGVNFS0KD3TDyG4hcvz6r4dQqeO2caJ6lxoP7P9eGQ98jwuLjYwJDl3Rz3tTcSAlFy72Nnihf+tr3zn6CcCzGVCQoRStIk164Y5WcHWwwaG0PPwYn6x2OETmty38j1eUcccHgaAotSMye0xw6B8KSyvw3mplLcbTt7eAj+t1mpja2AMD3lbG278ALp5pgCipvnm72OPZvi3l+IM/jyG/pFztkIjMx8EfgZQ4wNYZ6DtN7WgsAhMc+gexm+ZcfimaejthTM/Qun1S68FAWB+gslQ5x0yaNDomFGGNnXG+oKxm9xwR1cO28HWvK+NbngfcAtSOyCIwwaFasvJKMGfzaTl+ZWAb2NvUsdO0OJfc/y1l23jCT8q5ZtIcUan6lUFt5PjrrYnIyC1ROyQi7YubDeQmK/2mYiapHY3FYIJDtXz81wkUl1eiU4gHBkb8bVv49fi3BzqOVMZrpirnnElz7gj3Q9dQT1ke4OO1x9UOh0jbirKBLR8q49v+C9g6qh2RxWCCQzVOZuVj6a6kmqJ+V9wWfj3iF9jaHji7FTixpv6DJKMT/++vDFKK/y2LT8axjHy1QyLSri3/B5TkAr7tLr0BpAbBBIdqvPvHMYjdwf3lO3gvwx7EIxjo/oQyXjsN0FXWa4zUMKKaemJQhL/8eahecE5EN+jiWeX0lHDHG4BVHU/5U71ggkOSKO7215FMWFs1wksDlTUYBus1GXD0BM4dAfZV1XwgzXlxQGvYWDXC+qNZ2H7qvNrhEGnPhreVbuHNbgVa9FU7GovDBIeg1+vxzu9H5HhE12C08HW5uQd09ABu+felX/Ay9jfSojAfFzwUHVLTskMUfySiOkrfDxxYeqklA4v6NTgmOIQ/EjKwLzkHTnbWeK6fUgflpnUbD3iEAPnpwI4v6+cxqcE907clnO2sZdHH3w6mqx0OkTaIDRZrXlPGoh1DYCe1I7JITHAsXHmlThZ1E8bfEgZfV4f6eWBR/O/2qcp46ydA4YX6eVxqUI1d7PHErc3lWPyciJ8XIrqO0xuAxE2AtR1w+6tqR2OxmOBYuJ/iU5B4vhDeznYY3zusfh884l5l63hZPrDtk/p9bGow425pJhOdpOwi/LCbLRyIrjt7U13stMs4wLOOxVKp3jHBsWAl5ZX4dN0JOZ7Yp7nsO1WvrKyA26umacVOgjye4tAiJzsbTLpNmcX5bN0J+XNDRFdxdBWQtldpyXDLC2pHY9GY4Fiw73cmIT23BAHuDni4e1PjfJGW/YHgaKCi5FKxK9KcB6ND0MTDEZl5pfhux1m1wyEyTaIsxvqqvnzdJwIuPmpHZNGY4FhwQ80ZG07WLCR1sDVSfQaxc6Bv1Vqc+PlsxKlRomXHs1UL0L/ceAoFpRVqh0Rkmg01RXkMB3egx9NqR2PxmOBYqPnbz+BCYZlsqHlfVJBxv1hoLyDsNkBXAWx8z7hfi4zmnk5NEObjjOzCMszbmqh2OESmpbIc2PiOMu75rFIug1TFBMcC5RaV13SKnnxHK9haN8CPQd+qtTgHlgDnlF1bpC021lby50UQDVlzisrUDonIdOz9VpmhdvYBoququZOqmOBYoNlbTiG/pAKt/VwxtENgw3zRJlFAmzsBvU4p/keaNDgiAG0D3JBfWoGvNild54ksXnkxsOkDZSyKnNo5qx0RMcGxPOfySzFvq7IO5oX+rWBl1YDVNUUjTjQCDv+i7DIgzRE/Ly8OUGZx5m9PRFZeidohEalv19dAfhrgFgR0Gat2NFSFCY6FEaemissr0THIHXeE+zXsF/cLV6p6ChumN+zXpnpzW2tf2YyzpFwnFxwTWbSyQmDrR8q4z8tKkVMyCUxwLEhWfknNFt/J/VujkRq9Ufq8AjSyAk78CaTGN/zXp5smfm6q1+J8H5eETM7ikCXbNRcougB4NgM6PqR2NNTQCc6MGTMQGhoKBwcHREdHIy4u7qr3nT9/vvwDevkhPu/vzSGnTp2KgIAAODo6ol+/fjhxQilYR1f31cbTKK3QoVOIB3q3bKxOEN7NgQ4jlPHGd9WJgW5aj+be6BrqibIKHWZyFocsVWkBsO1TZdz7RcC6noulkmknOEuXLsXkyZMxbdo07NmzBx07dsSAAQOQlZV11c9xc3NDenp6zXH2bO3CYu+//z4+++wzfPXVV9i5cyecnZ3lY5aU8J3k1Yi1Eot2Ks/j8/1aqTN7U038IWhkDZxYA6RwFkeLxM+P+DmqnsXJyOXvHln47E31GzeynATno48+wvjx4zF27FiEh4fLpMTJyQnz5s275h9Pf3//msPPz6/W7M0nn3yCV199FXfddRc6dOiAhQsXIi0tDStWrDD2t6NZYseLmL3pHOKBW9SavbnSLM4mzuJoVUxzb3QL9ZKzONVlB4gsavZm+2fK+NaXOHtjaQlOWVkZ4uPj5Smkmi9oZSUvx8bGXvXzCgoK0LRpUwQHB8sk5tChQzW3JSYmIiMjo9Zjuru7y1NfV3vM0tJS5OXl1TosdfbmObVnb6r1/vdlszi71Y6GDCB+jp6rqm7MWRyyOLvmKLM3XmFA+wfUjoYaOsE5f/48Kisra83ACOKySFKupHXr1nJ255dffsF3330HnU6HHj16ICUlRd5e/Xk38pjTp0+XSVD1IRInSzJz0yk5eyN2vqg+e3P5LE7HkcqYa3G0PYvTTJnFmblRaf1BZBlrb6pmb3pz9sZUmdwuqpiYGIwePRqRkZG49dZb8fPPP8PHxwezZs0y+DGnTJmC3NzcmiM5ORmWQuxwWbQzSY7Fu22TmL2pJjrtilmck2uB5F1qR0M3OYuzOC4Z6bnFaodEZHxxs4Hi7KrZm6rSF2RZCU7jxo1hbW2NzMzMWteLy2JtTV3Y2tqiU6dOOHlSeXdY/Xk38pj29vZy4fLlh6UQO1zKqmZverUwkdmbK83icC2OZsWEVc3iVHJHFVmA0nxg++fKmLM3lpvg2NnZISoqCuvWrau5TpxyEpfFTE1diFNcBw8elFvChWbNmslE5vLHFGtqxG6quj6mJdW9EWsjTGLn1PXW4pz8izuqzGBH1ZK4ZK7FIfOvWixnb5pz9sbST1GJLeJz5szBggULcOTIEUycOBGFhYVyV5UgTkeJU0jV3njjDaxZswanT5+W28offvhhuU388ccfvzQl/txzeOutt7By5UqZ/IjHCAwMxPDhw4397WjK3C2JcvZG1L3p2cIbJklM8VbvqNryodrR0M3uqKrUYc4W9qgiM1VWBMR+cekUO2dvTJrR/3dGjBiBc+fOycJ8YhGwWFuzevXqmkXCSUlJcmdVtYsXL8pt5eK+np6ecgZo+/btcot5tZdeekkmSRMmTEBOTg569eolH/PvBQEtWXZhWU3V4mduN7G1N393y2Rg/2Lg2O9AxkHAv73aEZEBJt3eAqPnxckde0/2aQ5vF5asJzOzZyFQeA7wCAE6cOeUqWukF4VlLIw4pSV2U4kFx+a6Huf/1hzD5+tPol2gG1Y93cu0Exxh2Vjg0M9Au7uB++erHQ0ZQPwpGT5jG/an5MoE56WBbdQOiaj+VJQCn0YqTTXv/Bjo8pjaEVmkvBt4/Ta5XVR083KLyzF/m9Ix/OnbW5h+clM93SscWgGcO652NGQA8XM26XZlR9XC2LPILSpXOySi+rPveyW5cQ0AIkepHQ3VARMcM/Rt7Bnkl1agpa8L+ofXbbea6vwjgNZDxDzApc68pDl92/iijb8rCkorMH+7kmQTaV5l+aW/Sz2fZcdwjWCCY2YKSyvw9dbEmjURVlYamL2p1rtqFufAD0C28j2QtoifN/FzJ8zbligTHSLNO/gjkJMEODUGOj+qdjRUR0xwzMz3O5Nwsagcod5OGNJe2VqvGU2igOZ9AX0lsO0TtaMhAw2KCECYj7M8VVq90J1Is3SVwJb/U8Y9JgF2TmpHRHXEBMeMlJRXYnbVFt0n+7SAjbUG/3tFp3Fh7yIgN1XtaMgA1laN8FQfZRZn7pbTKC6rVDskIsMd/gW4cAJw8AC6jFM7GroBGnwFpKtZtjsZ5/JL0cTDEcM7NYEmNY0BmvYCdOWXOvWS5gyLDESwlyPOF5RhcVWxSSLNEZuMq2dvuk8EHMxz1625YoJjJsordfhqkzJ7869bw2Bno+H/WlHdWIhfABReUDsaMoCttRWeuLV5zSyOKDhJpDkn1gKZCYCdCxD9L7WjoRuk4VdButxvB9KRmlOMxi52eKCLxrulh/UBAiKBimIgzvAmq6SuezsHwcfVHmm5JVi5P03tcIhu3NaPlY9dxgKOnmpHQzeICY4Z0On0NU0Ox/ZsBgdba2iaqNsjqhsLO2cpze1Ic8TP4bhezeT4q02n5M8pkWYk7QCStgPWdkD3p9SOhgzABMcMbDiWhWOZ+XCxt8HD3ZvCLLS5E/BuAZTkKKeqSJNGRYfA1cEGJ7MKsPZIptrhEN347E3HBwE3je1IJYkJjhmUx/+yavZGJDfujrYwC1bWSkEtQTS3E2XSSXNcHWwxOkZJusXPqQV2hiEtyjwEHF8NNLK69HeINIcJjsbtOnMR8WcvykXFj/UMhVkRXcZdA4H8dODAUrWjIQON6dEM9jZW2J+cg9jTXDROGrC1qg5X+F2At7JYnrSHCY7Gzdx4Un68LyoIvm5m1k1dlEOPqTr3ve1TpeAWaY5YaFy98L16rRiRybp4Bkj4SRn3el7taOgmMMHRsMNpedhw7BxEN4Z/9Q6DWYp6VCmwdeEkcORXtaMhA03oHSYLAG45cR4HU3LVDofo6rZ/rlRTF1XVAzqqHQ3dBCY4GiZ2pghDOgSiqbczzJK9K9BtwqVFf1zDoUnBXk4Y2iGg1s8tkckpyAL2fqeMOXujeUxwNCo5uwirDii1RSZWFVQzW9FPADaOQPo+4PRGtaMhAz3RR/k5/T0hHYnnC9UOh+ifRFmKihIgqCsQ2kvtaOgmMcHRKFEdVpQV6d3KB+GBZl4+3Nkb6DxaGbN9g2a18XfD7W185SSc+PklMimlBcCuucpY7JwS9bhI05jgaFB2YRmW7k6W4yfMde3N38U8qWzZPLUeyDiodjR0E2txhB/jU3C+gFv/yYTs/VapuyXqb7UeonY0VA+Y4GjQt7FnUVKuQ0QTN8Q094ZF8AwFwocr422cxdGq6GZe6BjkjtIKHRZuP6N2OESKynIgdoYyjpkEWPGl0Rzwf1FjSsorsSBWeWH4V+/maGRJ06g9n1E+ii2cOexQrUXi5/VfVWvGFu44i6KyCrVDIgIOrQBykwFnH6VyMZkFJjgasyw+RZ6iCvJ0xKAIf1iUwE5As97KFs4dM9WOhgw0oJ0/mno7IaeoHD/sUk61EqlGLAoTdbYE0THc1szqiVkwJjgaUqnT1yzOHH9LGGysLfC/r7psuuhPVXxR7WjIAKIezuO3KGtx5m5NREWlTu2QyJKd3gBkHgRsnYEu49SOhuqRBb5CatefhzJw9kIRPJxscX+XIFgkUXzLLwIoLwR2fa12NGSg+6OC4OVsh5SLxfg9IUPtcMiSVc/eiJ2aTl5qR0P1iAmORogmhbM2K7M3o2NC4WRnA4sk1hz1eOZSzYryErUjIgM42Frj0Rild9rszWzCSSpJ36/U1mpkrezUJLPCBEcj4hKzZbNC0bTw0aruzBYr4h7ALQgozGITTg17JKYpHGytkJCah9hTbMJJKrVlqP6b4hGidjRUz5jgaMTsqtkb0VTT28UeFs3aFug+8dIfKB3XcGiROEVV3YSzenaSqMHkJAMJPyvjHk+rHQ0ZARMcDTiZVYB1R7Pk2ZnqxZkWTzThtHcHLpwATqxROxoy0LhezeTP9abj53A8M1/tcMiS7PxK2ZHZ7FY21TRTTHA04OutifJjv7Z+aNbYTJtqGtKEUyQ5QuwXakdDBhJNYgeEK+UO2L6BGkxJrrITU+DsjdligmPiRDn7n/ek1GwNp7814bSyAc5sAdL2qh0NGWh872by44q9acjK56JxagB7FgJl+YBPG6BFP7WjISNhgqOBtgyirL0ob9811FPtcEyLexOg3T3KeDtncbQqqqkXOoV4oKxSJ3/eiYzelqG6UGjMU2yqacaY4Jh4W4Zvdyh/8MXaG4tqy1BXPSYpHw8tVxYNkiZVz05+t+Msissq1Q6HzL0tQ16q0pah/QNqR0NaT3BmzJiB0NBQODg4IDo6GnFxcVe975w5c3DLLbfA09NTHv369fvH/ceMGSNf7C8/Bg4cCHPz855U2ZahiYcFtmWoK7E4MPQWZbGgWDRImm3fEOzliItF5fix6pQsUb0T9ZZiq7aGd2NbBnNn9ARn6dKlmDx5MqZNm4Y9e/agY8eOGDBgALKysq54/40bN+LBBx/Ehg0bEBsbi+DgYPTv3x+pqam17icSmvT09Jpj8eLFMCc60ZZhq7Lo8rFezSyzLUNdVS8SFOfVS/LUjoYMbN8wrqeyFmfe1kT5809U785sVYr72TgCXdmWwdwZ/VXzo48+wvjx4zF27FiEh4fjq6++gpOTE+bNm3fF+y9atAhPPvkkIiMj0aZNG8ydOxc6nQ7r1q2rdT97e3v4+/vXHGK2x5xsOJaF0+cK4epggxFdlVohdBUt7gAatwJK85QkhzTp/i7BcHOwQeL5Qvx1JFPtcMgcVe+4jHyIbRksgFETnLKyMsTHx8vTTDVf0MpKXhazM3VRVFSE8vJyeHl5/WOmx9fXF61bt8bEiRNx4cLVK6GWlpYiLy+v1mHq5lRtmX2oWwhc7C20LUNdWVkpiwUFcZqqskLtiMgAzvY2GNVdqdI9d4tSGoGo3pw7DhxfLfq9XPp7QWbNqAnO+fPnUVlZCT8/v1rXi8sZGXVrsPfyyy8jMDCwVpIkTk8tXLhQzuq899572LRpEwYNGiS/1pVMnz4d7u7uNYc47WXKElJzseN0NmysGmFMT6VfD11Hh5HKosHcZODwCrWjIQON6REKW+tGiDujtCYhqjc7vlQ+th4MeDdXOxpqACa9sOPdd9/FkiVLsHz5crlAudrIkSMxbNgwtG/fHsOHD8eqVauwa9cuOatzJVOmTEFubm7NkZycrInCfkM6BCDA3VHtcLRBLBbs+vilP2Rs3qhJfm4OGNohsNbvAdFNK7wA7K9ap8nZG4th1ASncePGsLa2RmZm7fPp4rJYN3MtH374oUxw1qxZgw4dOlzzvmFhYfJrnTx58oq3i/U6bm5utQ5TlZlXgl/3p9WUsacb0GUcYG0PpMYDyVffqUemTSyqF34/mI703GK1wyFzED8PqChRdl027aF2NGQOCY6dnR2ioqJqLRCuXjAcExNz1c97//338eabb2L16tXo0qXLdb9OSkqKXIMTEBAArVsYewYVOr0s6tchyEPtcLTFxQfocL8y3jFD7WjIQBFN3NE9zEv+HizYzsJ/dJMqSoG4Ocq4Owv7WRKjn6ISW8RFbZsFCxbgyJEjckFwYWGh3FUljB49Wp5CqibW1Lz22mtyl5WonSPW6oijoKBA3i4+vvjii9ixYwfOnDkjk6W77roLLVq0kNvPtUwUOFu0M0mOOXtjoO5PKh+P/Apc5IujVo3rpRT++37nWRSWctE43QTRMbwgE3DxB9rdrXY0ZE4JzogRI+TppqlTp8qt3/v27ZMzM9ULj5OSkmQdm2ozZ86Uu6/uu+8+OSNTfYjHEMQprwMHDsg1OK1atcK4cePkLNGWLVvkqSgt+2lPCnKKymXBszuqGhDSDfJrB4TdBuh1QNxstaMhA/Vt44tQbyfklVTI3wsig4i1eNWzudETABs7tSOiBtRIr7e81Zhim7jYTSUWHJvKehxR2Kzfx5tk7Zupd4bXrEMgA5xYCyy6D7B3A54/BDiYxv8x3ZgF289g2spDaNbYGesm3worK55aoBuUuAVYcKdS2G/yYda+sbDXb5PeRWVJNh0/pxT2s7fBAyzsd3Oa971U+G/vd2pHQwa6LyqopvDf+qNXrnxOVKet4ZEPMrmxQExwTER1WwZRtZiF/eqh8F/3iZcK/+nYvFGrhf8ejA6p9ftBVGcXTgHH/qi9No8sChMcE3AkPQ/bTl6AmIF/tAcL+9Vb4T9HTyDnLHD0N7WjIQM9GhMq+1SJwpeH0nLVDoe0ZMdMsQgHaNkfaNxS7WhIBUxwTIBoLigMjBAdlZ3UDsc82DkBXR6rPU1NmhPo4YjB7ZXyDyz8R3VWfBHYt0gZc/bGYjHBUdn5glL8so+F/Yyi63jAyhZIigVS96gdDRmo+vdCFMDMyi9ROxzSAtF0t7wI8A0HwvqoHQ2phAmOyr7fmYSySh06Bnugc4h5dURXnVsAEHHPpbU4pEmR8nfDA+WVeizaodSJIroq0Wy3prDfkyzsZ8GY4KiotKIS3+5QitE91jMUjfiLWP+in7hU7Cu/bg1eyfRUl01YtPMsSsq5aJyu4egqpemukzfQvqqyOVkkJjgq+u1AOs7ll8LPzR6DIrTfZsIkNekMBHcHdOXArq/VjoYMNLCdPwLdHXC+oKymVxvR1RcXi950jylNeMliMcFRiaivWL1ocnRMKOxs+F9hNNVbxnd/DZRzDYcW2VhbYXTVDsN5287I3x+if5CNdncoa+9E812yaHxVVcmuMxdxKC0P9jZWeLCbUuuDjKTNnYB7MFB0ATi4TO1oyEAjuwbD0dZallUQ28aJ/mFH1Vo7sfZOrMEji8YERyXfbFNmb+7p3ARezuyPYlTWNkC3CZemr/nuX5M8nOxwb1STWr8/RDXy0oFDy2vP2pJFY4KjguTsIvx5SFnwOrYnt4Y3iM6PALbOQNYhIHGz2tGQgcb0UH5f1h7JRNKFIrXDIVMiTkGLtXYhMUBgJ7WjIRPABEcFC2PPQKcHbmnZGK38XNUOxzKIqsaRD9VehEia08LXBbe28pGTcPO3n1E7HDIV5cXA7nnKmLM3VIUJTgMrKK3Akl3JcvwYZ2/U2TJ+fLXSp4Y0vWX8h93JyC8pVzscMgVibZ1YY+ceArQeonY0ZCKY4DSwn+JTkF9SgbDGzvKdKDWgxi2AlgOU/jQ7Z6kdDRmod8vGciZHvFlYtjtF7XBIbWI6r3pxcbfxypo7IiY4DUun09dMq4/pGQor0V2TGlb3qlkc0aemhM0btUgUxBxTtWV8QewZVIrzvWS5xJo6sbZOrLHrPFrtaMiEMMFpQJuOn0Pi+UK4Otjg3s5BaodjmcJuAxq3BsoKgL1VzfhIc8TuQzcHG5y9UIQNR7PUDofUVN2GJfJBwNFD7WjIhDDBaUDfVM3ejOgSDGd7TqOqQrTDiP6XMo6bBehY9l+LnOxsaupHfbOdW8YtVnYicOyP2mvsiKowwWkgJ7MKsPn4OYizUo9WTa+TSjqOBBzcgYtngBNr1I6GDPRITFP5+7Tt5AUcy8hXOxxSg2yqqQda9AMat1Q7GjIxTHAayPyqd5n92voh2MtJ7XAsm504V/+oMuaWcc0K8nTCgHb+tX6/yIKU5gN7v1XG0dwaTv/EBKcB5BaV46f4VDlmYT8TIXZbNLICEjcBmYfVjoYMVP379POeVFwsLFM7HGpI+xYDpXmAd0ug+e1qR0MmiAlOA1i6OwnF5ZVo4++K7mFeaodDgkeI0qPq8kWKpDldQz0RHuCG0godFu9KUjscaig63aXfW7GmzoovZfRP/KkwsopKHRZsPyvHY3uGyi2uZCKqK54eWAoUsXmjFonfJ/F7JXwbexbllTq1Q6KGcPIvIPsUYO8OdHxQ7WjIRDHBMbK/jmQiNacYnk62uCtSaRRIJkL0rPHvAFSUAPHz1Y6GDDS0YyC8ne2QnltS0+ONzNzOmZd6zNm7qB0NmSgmOEY2b5uyNfyh6BA42FqrHQ5dTsymVc/i7JoLVLLsvxaJ36tR0VVbxqt+38iMnTsGnFqvrKETa+mIroIJjhEdSstFXGI2bKwa4ZHu3BpuktrdAzj7AHmpwJFf1Y6GDPRw96awtW6E+LMXcSAlR+1wyJiq1960Hgx48u8qXR0THCOaX/VuclD7APi7O6gdDl2JrQMQNVYZsz+VZvm6OWBI+4Bav3dkhoovAvuXKGMW9qPrYIJjJBcKSvHL/jQ5ru6bQyaq6zjAygZI3gGk7VU7GrrJLeO/HkhDVn6J2uGQMez5FigvAvwigNBeakdDJo4JjpEsjktCWYUOHYPc0TmE/VFMmqs/0O5uZcxZHM3qGOyBTiEeKK/U4/ud3DJudkRbFVm5uGprOHek0nUwwTECsVX12x1na7qGc2u4BlRXQk34CShg80atz+J8tyMJpRXsM2ZWjv0O5CYBjl5A+/vVjoY0gAmOEfyRkIHMvFL4uNpjSPtAtcOhugiKApp0ASrLgN3fqB0NGWhQhD/83OxxvqAUvx9MVzscqk/Vs6tRYwBbR7WjIQ1okARnxowZCA0NhYODA6KjoxEXF3fN+y9btgxt2rSR92/fvj1+//33Wrfr9XpMnToVAQEBcHR0RL9+/XDixAmYim+2KX1xxNZVOxvmkJpRvWV899dABcv+a5GttRUe6d60Zsu4+FtBZiAjATizBWhkDXR9XO1oSCOM/uq7dOlSTJ48GdOmTcOePXvQsWNHDBgwAFlZVz4NsH37djz44IMYN24c9u7di+HDh8sjISGh5j7vv/8+PvvsM3z11VfYuXMnnJ2d5WOWlKi/sHBfcg72JuXILaujopU/tKQRbYcBLv5AQSZweIXa0ZCBHuymvLE4kJKLPUncMm5WW8PDhwHuLJhKJpLgfPTRRxg/fjzGjh2L8PBwmZQ4OTlh3rx5V7z/p59+ioEDB+LFF19E27Zt8eabb6Jz58744osv5O3iHdknn3yCV199FXfddRc6dOiAhQsXIi0tDStWqP+iNL9q9mZoh0B5ioo0xMbu0rtD0WWc7/41ydvFHnd1DKw1m0oaVngBOLhMGbNrOJlKglNWVob4+Hh5CqnmC1pZycuxsbFX/Bxx/eX3F8TsTPX9ExMTkZGRUes+7u7u8tTX1R6ztLQUeXl5tQ5jyMorwW9V5/3ZNVyjxPl9azsgbQ+QslvtaMhAYnF/9Xq49NxitcOhm7FnvtJOJSASCO6mdjSkIUZNcM6fP4/Kykr4+fnVul5cFknKlYjrr3X/6o838pjTp0+XSVD1ERwcDGP4bmeS3KIa1dQT7YPcjfI1yMhcfC7t0Kjud0Oa0y7QHd2aeaFSp8d3VTsaSYNE+5S4uZfWyHFHKt0Ai1gBO2XKFOTm5tYcycnJRvk690cF4fFezfCv3mFGeXxqIKLGhnD4FyBPKdZI2vNY1SyOqIlTUs4t45ok2qfkpwHOvpdqVRGZQoLTuHFjWFtbIzMzs9b14rK/v/8VP0dcf637V3+8kce0t7eHm5tbrcMYgr2c8Oqd4ejf7spxkEYEdARCegC6CmDX12pHQwbq19YPTTwccbGoHCv3MVHV9OLiLo8BNlzTSCaU4NjZ2SEqKgrr1q2ruU6n08nLMTExV/wccf3l9xfWrl1bc/9mzZrJROby+4g1NWI31dUek+iGda/qcxP/DVCu/u48unE21lYYHVO1ZXw7t4xrTuoeIHknYGWrJDhEpnaKSmwRnzNnDhYsWIAjR45g4sSJKCwslLuqhNGjR8tTSNWeffZZrF69Gv/3f/+Ho0eP4n//+x92796NSZMmydtFVeDnnnsOb731FlauXImDBw/KxwgMDJTbyYnqReshgHswUHQBSPhR7WjIQCO6BsPB1gpH0vOwMzFb7XDIkMJ+EfcArrXXXBKZRIIzYsQIfPjhh7IwX2RkJPbt2ycTmOpFwklJSUhPv1RxtEePHvj+++8xe/ZsWTPnxx9/lNu/IyIiau7z0ksv4emnn8aECRPQtWtXFBQUyMcUhQGJ6oW1zaUt42KanO/+NcnDyQ73dA6SY3YZ15D8TKVtyuVr4ohuUCO9Bc7bilNaYjeVWHBsrPU4ZAaKsoGPwoGKYmDM70BoT7UjIgMcz8xH/483w6oRsOnF2+RaOTJxG98FNk4HgroBj69VOxrS6Ou3ReyiIjKIkxfQcYQy5pZxzWrl54peLRpDp0dNE1wyYRWllxb3V6+FIzIAExyia+lWNT1+9DcgJ0ntaMhAY3ooW8aXxCWhqKxC7XDoWg6tAAqzANdApX0KkYGY4BBdi1840OxWQK8D4uaoHQ0Z6PY2vmjq7YS8kgr8vCdV7XDoasSKierZ0q7jAGtbtSMiA5RV6HDmfCHUxgSH6Hqiq6bJ9ywAytT/paUbZ2XVCKNjlFmc+dwybrpSdgFpewFre6VtCmnSbwfTcNv/bcSUnw+oGgcTHKLraTUA8AwFSnKBA0vVjoYMdH+XIDjbWeNkVgG2njyvdjh0JaLJrSDapTg3VjsaMoB48/DNNvEmArLQppqY4BBdj5X1pbU4ojYH3/1rkpuDLe6LUraMiz/AZGJyU5X2KAIXF2vWnqSLOJCSCzsbKzzYLUTVWJjgENVFp1GAnQtw7ihweoPa0ZCBHq1abLz+aBYSTWCNAF1m11xAXwk07QX4t1c7GjLQvKo3D8MjA+Htom57DSY4RHXh4A5EjlLGO6r645DmhPm44LbWPnK8YDtncUxGeTEQP18Zc/ZGs9JyirE6IUOOx/RopnY4THCI6kxWVG0EnPgTuHBK7WjIQGN7Kn94l+1ORl5JudrhkHDgB6A4G/AIAVoPVjsaMpCoM1Wp0yO6mRfCA9UvossEh6iuvJsDLfvX7pNDmnNLy8Zo4euCwrJKLNudonY4JLeGV82KdpugrHkjzSkuq8TiuKRabyLUxgSH6EZUT5/vW6TsqiLNEQ17qwv/idNU4h0nqShxM5B1GLB1Bjo9onY0ZKAV+1KRU1SOIE9H3BFuGs1RmeAQ3Yiw2wCfNkBZAbB3kdrRkIHu6dwEbg42SMoukguOSUXVszeRDwKOHmpHQwZvDU+U40djQmEtGr+ZACY4RDeiUaNL3Y3jZgG6SrUjIgM42dnUbGGt/sNMKsg+DRz7o3ZBTdKc7acu4HhmAZzsrPFA12CYCiY4RDeqw0jAwQO4eAY4/qfa0ZCBHolpKjuMiz/ORzPy1A7HMsn2J3qgRT+gcUu1oyEDVdeVurdzENwdTae9BhMcohtl5wREPaqMd3ypdjRkoCBPJwxo5y/H81n4r+GV5AF7v1PG0RPVjoYMdPZCIdYdzaxVZ8pUMMEhMkTX8UAja+DMFiAjQe1oyECP9VJ2eyzfm4rswjK1w7Es+74HSvOAxq2A5rerHQ0ZSOntBvRp7SN3J5oSJjhEhvAIBsKHKePq7sekOV2aeqJ9E3eUVuhqtrhSAxBr16oXF4u1N1Z8KdKi/JLymlILj5nI1vDL8aeKyFDdn1Q+HlgGFJxTOxoycMv4Y72UafWFsWdQVqFTOyTLINauXUxU1rJ1HKl2NGSgH3anoKC0Qs7ciPpSpoYJDpGhgroCgZ2BylIg/hu1oyEDDWkfCB9Xe2TmleKPhHS1w7EM1WvXosYAds5qR0MGEPWj5m9PrJm9EW8WTA0THCJDiV/o6lkc0Siwgms4tEh0PR7dvakcf701Udb0ICPKOKisXRNr2LqNVzsaMtBfRzKRnF0MDydb3N2pCUwRExyimxF+F+AaABRkAoeWqx0NGeih6BCZ6BxIycWepItqh2PeqpvVijVs7kFqR0MGmrdVmb15qFsIHO1Ms70GExyim2FjB3R9XBnvmKH01SHN8Xaxx/DIQDmet5Vbxo1GrFU7+IMyrp79JM05lJaLnYnZsLFqJOtJmSomOEQ3K2osYOMApO8HknaoHQ0ZqLpB4OpDGUjNKVY7HPMk1qpVlgFNopQ1bKTpwn6D2wcgwN0RpooJDtHNcvYGOjygjLllXLPaBrihR3NvuXhS7KiielZRqqxVq569McFFqXR95/JLsXJfWq06UqaKCQ5RfaiuxHrkV+DiWbWjIQNV1/JYvDMJhaUVaodjXhJ+VtaqiTVrYu0aadJ3O86irFKHziEeiAw27eaoTHCI6oNfOBDWB9DrgLjZakdDBrq9jS9CvZ2QV1KBn/YoBcyoHoi1aWKNmiDWrFmbTr8iqruS8kqZ4Fx+SteUMcEhqi/dn1I+7lkIlOarHQ0ZwMqqUc0fbrHOQKfjovF6cWarsj3cxhHo8pja0ZCBxKmpC4VlCHR3wKAIpY+bKWOCQ1RfREdk75ZKf53qJoKkOfdFBcHNwQaJ5wux/miW2uGYV2G/yAcBJy+1oyEDiPpQ87Yl1jTVtLE2/fTB9CMk0grRTyemauvrjplKvx3SHGd7GzzYLaSm8B/dpAungGN/KGNuDdesbScv4GhGPpzsrDGy6vfD1DHBIapPHUYCjp5Azlng6G9qR0MGEu9Qra0aIfb0BVnzg26CbKqpB1r2Bxq3VDsaMtDXW0/Ljw90CYa7ozbWUDHBIapPdk6X1hhUT8uT5gR6OMoaHwJncW5C8cVLp2s5e6NZJ7PyseHYObmzf2xPpTktLD3Byc7OxqhRo+Dm5gYPDw+MGzcOBQUF17z/008/jdatW8PR0REhISF45plnkJtb+x2UaOr192PJkiXG/FaI6q7reMDKFkiKBVLj1Y6GDDSuqsbHr/vTkJVXonY42hS/ACgvAnzbKbsMSZPmVRX269fWD029tdMc1agJjkhuDh06hLVr12LVqlXYvHkzJkyYcNX7p6WlyePDDz9EQkIC5s+fj9WrV8vE6O+++eYbpKen1xzDhw835rdCVHduAUDEvco4lrM4WiVqfEQ19UR5pR7fVm2NpRtQWX6pZIJYm8bCfpp0sbAMP1eVTKhO+mHpCc6RI0dkcjJ37lxER0ejV69e+Pzzz+VMi0hiriQiIgI//fQThg4diubNm+P222/H22+/jV9//RUVFbWLbokZIX9//5rDwcHBWN8K0Y2rXmx8eAWQm6p2NGSg6j/oi3YmyRogdAMO/wLkpQLOPkDEfWpHQwb6Pk787OvQLtAN0c20tQPOaAlObGysTEK6dOlSc12/fv1gZWWFnTt31vlxxOkpcYrLxsam1vVPPfUUGjdujG7dumHevHlyC9vVlJaWIi8vr9ZBZFQBHYGmvQBdBRA3S+1oyED9w/3QxMMR2fJdLBPVGyvs9+Wlwn62fAOqRWUVOizYfqYm2RfLQbTEaAlORkYGfH19a10nkhQvLy95W12cP38eb7755j9Oa73xxhv44Ycf5Kmve++9F08++aScHbqa6dOnw93dveYIDg428LsiugExVYX/4ucDpVdfe0amS9T6qF5UKXaRsPBfHYmms2L9mbU90OWfSwxIG34V68/yS+Hrao87OwRCa244wXnllVeuuMj38uPo0aM3HZiYZRkyZAjCw8Pxv//9r9Ztr732Gnr27IlOnTrh5ZdfxksvvYQPPvjgqo81ZcoUORNUfSQnJ990fETX1Wog4NUcKMll4T8NG9E1GK72Njh1rhAbj7PwX53EfqF87DgCcPFROxoygDgrMmfL6ZqyCXY22tt0fcMRv/DCC3J9zbWOsLAwuS4mK6v2HwOxjkbslBK3XUt+fj4GDhwIV1dXLF++HLa2195zL9b4pKSkyFNRV2Jvby9Pc11+EDVs4b8vWfhPo1wdbDGymzLrO2czt4zXqbBfdQ2omElqR0M3WdjP0dYao6K1Udjv72ovbKkDHx8feVxPTEwMcnJyEB8fj6ioKHnd+vXrodPpZEJyrZmbAQMGyKRk5cqVdVo8vG/fPnh6esrPITIpHR8C1r+tFP4TncbbcbefFo3p2UxulRWF/xJScxHRxF3tkEyXXHtTVdjPp7Xa0ZCBqmdvHugSBA8nO2iR0eac2rZtK2dhxo8fj7i4OGzbtg2TJk3CyJEjERionMtLTU1FmzZt5O3VyU3//v1RWFiIr7/+Wl4W63XEUVmpvPsVO6rEziyxjfzkyZOYOXMm3nnnHVk/h8gkC/91HVd72p40Ryw0HlJV+G9u1R9+uoKibGDvImXM2RvNOp6Zj03HlcJ+j2lsa/jljHpSbdGiRTKB6du3LwYPHiy3is+eXVUXAUB5eTmOHTuGoqIieXnPnj1yh9XBgwfRokULBAQE1BzV62bE6aoZM2bIGaLIyEjMmjULH330EaZNm2bMb4Xo5gr/WdsBKbuApLrvICTTMv6WMPlx1YF0pOUUqx2Oadr9NVBRDPi3B5r1VjsaMlB1Ej8g3F9Thf3+rpH+WvurzZSYGRK7qaq3oBMZ3S9PKQuN2w4FRnDBsVaNmBWLnYnZmNA7DP8Z3FbtcExLRSnwSXugIBO4e7aywJg0Jyu/BL3e3YCySh1+mhiDqKZemn391t6yaCItqp6uP7IKyOYpDq3P4izemYT8knK1wzEtB5cpyY1rIBBxj9rRkIEWbj8rk5tOIR4ml9zcKCY4RA3Bty3Qop+y+HLHTLWjIQPd3sYXYT7OyC+twNJdLDdRQ5wIiJ2hjLs/AVhro9s01VZUVoHvdiptSSZUJfNaxgSHqKFnccSpKrEYkzTHyqoRHu+l/OH/ZtsZVFTq1A7JNJxaB2QdBuxcgM6Pqh0NGein+BTkFJUjxMsJ/dtdu5yLFjDBIWooopuyX3ulu/LueWpHQwa6p3MTeDvbITWnGL8n1K0qu9nbXlVJvvNowNFD7WjIAJU6PeZuVeo8PdYzFNZW2mrLcCVMcIgaithz2aOqnMHOWUB5idoRkQEcbK0xOkZp3zBr06lr9sGzCGn7gNMbgUbWQPeJakdDBvrzUAbOXiiCh5MtHuhqHu2MmOAQNSSx+NItCCjMAg4sUTsaMtAjMU3hYGuFQ2l52H7qAixa9eyN+Nn20GbFW0un1+sxa7Oy+eGR7k3hZHfDNYBNEhMcooYkFl9Wv8sVLww6ruHQIi9nOzzQRXmXW/3CYJEungUOLVfGPZ5ROxoyUFxiNvYn58h+U9Wzk+aACQ5RQ4t6FLB3By6cBI79rnY0ZCCx2FgsU9h8/BwOp+XBYtsy6CuBsNuAgA5qR0MGmlWVpN8XFQQfV/NpecQEh6ih2bsCXR9Txts/UzsaMlCItxMGVbVvqO7bY1HETsA9C5VxT87eaLktw/qjWXKJYHWdJ3PBBIdIDdGiVogdkLwTSNqhdjRkoH/1Vl4QVu5Pk7uqLMqur5UdgaItg5jBIU2avflSW4ZmjbXbluFKmOAQqcHVH+hQVcp+G2dxtKpDkAdiwrzlFtt5VVtsLYLYARg3Sxn3eFbZIUiak5Fbgl/2pcrxhFvNa/ZGYIJDpJbqRZliHc6542pHQwb6V9ULw5K4JOQWWUj7hv2LgcJzgHsI0G642tGQgb7ZnojySj26hXqhc4gnzA0THCK1+LQCWg9W2jfEVm21Jc25tZUP2vi7orCssqbMvVnTVV7aGh7zJNsyaFR+STm+35Ekx6J5rDligkOkpp7PKh/3LwHy0tWOhgzQqFGjmhcI0b6hpLwSZu2oaBh7CnDwADo9onY0ZKBFomFsaQVa+LrIHmvmiAkOkZpCugPB3YHKMmXLLWnS0I6BaOLhiPMFpfgxPgVmS1Rt3vqxMu42AbB3UTsiMkBJeSW+rlozJhbKix5r5ogJDpHabpmsfBT9qYovqh0NGcDW2grjb2lWsyvFbJtwipYMaXsBG0dlJyBp0s97UnEuvxSB7g64K7IJzBUTHCK1tewP+IYDZQXArrlqR0MGGtE1RFY4Tsouwm8HzfR0Y/XsjShW6eytdjRkgIpKHWZtPiXHj98SJqsXmyvz/c6ItEJsse31vDLeMRMoK1I7IjKAo501xvRQytzP3GiGTThT44HETYCVDRAzSe1oyEB/JChNNT2dbDGym3k01bwaJjhEpqBdVaPCogvA3u/UjoYM9GhMKJztrHE0Ix8bj5+DWc7etH8A8DDvF0ZzpdfrZfItjOnRzGyaal4NExwiU2Btc6kujtiCW2kh9VTMjLuTLR6KVjpqz9ygvJCYBVGn6ciq2jv/SHM2ib5p6XlwsrPG6JimMHdMcIhMRaeHAWcfIDcJSPhJ7WjIQON6hcHWuhHizmRj95lsmIVtnyr1mloPAXzbqB0NGWhm1ezNg91C4OlsB3PHBIfIVNg6At0nKuOtnwA6M92JY+b83R1wb+cgOf5qkxnM4uSmAAeWKuPqtWKkOfFnL2JnYrZMvh+v2vFn7pjgEJmSLuMAO1fg3BHg+Gq1oyEDicJ/Yu34X0eycDQjD5oWOwPQlQOhtwDBXdWOhm5y9ubuTk0Q4O4IS8AEh8iUOHoAXccp4y0fKoXVSHPCfFwwOCJAjr/U8lqcwvPA7m+Uca/n1I6GDHQkPQ9/HcmUSfe/bm0OS8EEh8jUiC24opCa2JZ7eoPa0ZCBnrqthfy46kAaTp8rgGZnbyqKgcDOQPO+akdDBpqx4aT8OKR9AJr7WE71aSY4RKbGxQeIGqOMN3+odjRkoPBAN/Rr6wud/tLpAU0RVbXj5ijj3i8q9ZpIc06dK6gpPFmddFsKJjhEpqjH04C1HXB2G3Bmm9rRkIGqX1CW701FcrbGCjjunA2U5QN+EUCrgWpHQwb6coMoOgncEe6HtgFusCRMcIhMkXsTIHLUpbU4pEmdQjxxS8vGqNDpa8rja0Jp/qXmr7e8AFjxpUKLkrOLsGJfqhxPsrDZG4E/tUSmSizqbGQNnFoPpMSrHQ0ZqPqF5YddKcjMK4Em7PoaKMkBvFsC4XepHQ0ZaOamU6jU6dG7lQ86BnvA0jDBITJVnqFAhxHKmLM4mhUd5o1uoV4oq9TJTuMmT/RCi/3istkba7UjIgOk5xbjx90pcvz07ZY3eyMwwSEyZbdMFt04gWO/AxkJakdDBppU9QKzaOdZXCgohUnbsxAoPAd4NAXa36d2NGSg2ZtPy6Q6upkXuoZ6wRIZNcHJzs7GqFGj4ObmBg8PD4wbNw4FBdfeLtmnTx80atSo1vHEE0/Uuk9SUhKGDBkCJycn+Pr64sUXX0RFRYUxvxUidTRuCbS7WxlzFkezxDqcjkHuKCnXYe7WRJisitKqtgxVVYutbdWOiAxwLr8Ui+OS5Pjp21vCUhk1wRHJzaFDh7B27VqsWrUKmzdvxoQJE677eePHj0d6enrN8f7779fcVllZKZObsrIybN++HQsWLMD8+fMxdepUY34rROrp/W/l46EVQNZRtaMhA4g3apOqXmgWbj+Di4VlMEl7vwXy0wDXQCDyIbWjIQPN2XJaJtNi3U3PFt6wVEZLcI4cOYLVq1dj7ty5iI6ORq9evfD5559jyZIlSEtLu+bnipkZf3//mkPMAFVbs2YNDh8+jO+++w6RkZEYNGgQ3nzzTcyYMUMmPURmx68d0Hao0uxw86Vkn7RF1MQJD3BDYVkl5m49bZqzN1s+ujR7Y2OvdkRkgPMFpVgYe0aOn+vbUibXlspoCU5sbKw8LdWlS5ea6/r16wcrKyvs3Lnzmp+7aNEiNG7cGBEREZgyZQqKiopqPW779u3h5+dXc92AAQOQl5cnZ4uupLS0VN5++UGkKbe+onxM+BnIOqJ2NGQA8ULzXD9lFmf+tjPINrVZHLH2Ji9Vmb3pPFrtaMhAszadqpm96dPaB5bMaAlORkaGXB9zORsbG3h5ecnbruahhx6SszMbNmyQyc23336Lhx9+uNbjXp7cCNWXr/a406dPh7u7e80RHBx8k98dUQPzjwDaDlNmcTZxFkerRLG1doFVszhbTpvm7I1Y2G7roHZEZODam293nJXj5/pZ9uyNQQnOK6+88o9FwH8/jh41fJ2AWKMjZmTELI1Yw7Nw4UIsX74cp04ZXiRLJEq5ubk1R3JyssGPRaSaW19WPh5azlkcTc/itJLjBdtNaBZHzN5Ur73p9Ija0ZCBZm++bPamlWXP3gg2N/oJL7zwAsaMqeqTcxVhYWFy7UxWVlat68VOJ7GzStxWV2L9jnDy5Ek0b95cfm5cXFyt+2RmZsqPV3tce3t7eRCZxSzOkZXApveA++erHREZuBZHzOIcSsuTi0FfHthG3YA4e2MWOHtTDzM4Pj4+aNOmzTUPOzs7xMTEICcnB/Hxlyqwrl+/HjqdriZpqYt9+/bJjwEBAfKjeNyDBw/WSp7ELi2xEDk8PPxGvx0ibenzyqUdVZmH1Y6GzGEW5/LZG6690fzam0jO3hh/DU7btm0xcOBAueVbzLhs27YNkyZNwsiRIxEYGCjvk5qaKhOi6hkZcRpK7IgSSdGZM2ewcuVKjB49Gr1790aHDh3kffr37y8TmUceeQT79+/Hn3/+iVdffRVPPfUUZ2nIMnZUydL5Yi3Oe2pHQzcxixPRxA1FZZVyFkc15SW1Z2+4c0qTsvJL8N1Ozt40aB0csRtKJDB9+/bF4MGD5Vbx2bNn19xeXl6OY8eO1eySEjM/f/31l0xixOeJ02H33nsvfv3115rPsba2ljV1xEcxmyMWIIsk6I033jDmt0JkemtxDnMWR9OzOH1NYBanuu6NWxPO3mjY7E2na2ZvbuXsTY1Ger1opG5ZxDZxsZtKLDi+vMYOkWb88KiS4LS5Exi5SO1oyADiT++wL7bhYGouJvQOw38Gt23YAMqLgU8jgYIMYPCHQLfxDfv1qV6IBq6939+A0god5o/tij6ta+9etuTXb/aiItKiPlOUHlVHVwGpe9SOhgycxZl8x6VZnAbvNB43R0lu3EM4e6Nhn68/IZObLk09OXvzN0xwiLTIt82lTuPr31I7GjKQKMQmXpjEC5R4oWowJXnA1qq1N31e5tobjUq6UIQlcUrZk38PaM21N3/DBIdIyzuqrGyAU+uAM9vUjoYMIF6QxAuTIF6oxAtWg9jxJVB8EfBuCXQY2TBfk+rdJ+uOo0Knl81cu4dZbs+pq2GCQ6RVXs2Azo8q4/VvikUdakdEBhAvTOIFSrxQiRcsoyvKBrZ/oYxv/y9gfcPl0MgEnMjMx/K9qXL8YlWSTLUxwSHSst4vAjYOQFIscPIvtaMhA1W/QK3YmypfuIxq68dAWT7g3x5oK0oOkBZ9tPa4fE8zoJ0fOgR5qB2OSWKCQ6RlbgGXdr+sewPQ6dSOiAwgXqDEC5VOr7xwGU1eOhBXVarj9qmAFV8CtOhASg7+SMiAWHLzQn/O3lwNf7qJtK7n84CdK5BxQGnjQJokXqjEC5Z44TqYkmucL7L5A6CiBAiOBlreYZyvQUb34RolCR4e2QSt/FzVDsdkMcEh0jpnbyDmKWW84W2gskLtiMgA4oXq7sgmcvzBmmP1/wWyE4E9C5Rx36lihXP9fw0yup2nL2Dz8XOwsRItP1qqHY5JY4JDZA5EguPoBZw/Duxj4T+tEj2qxAuXeAHbfvJ8/T64KCegqwDCbgNCe9XvY1ODFYd8d/VROX6gazCaejurHZJJY4JDZA4c3JQFx8KGd4CyQrUjIgOEeDvh4e5N5Xj6H0ehE4ty6kPaXiDhR2V8x+v185jU4FYnZGBvUg4cba3xXF/O3lwPExwic9F1HODRVKlOK+qckCY9fXsLuNjbyBYOvx5Iu/kHFFtt1rymjEVxyICON/+Y1ODKK3V4r2r2ZnzvMPi6OagdksljgkNkLkQ1WrG2Qtj6KVBwTu2IyADeLvaY2Ke5HH/w5zGUVlTe3AOK8gFntgDWdsDtr9ZPkNTgFscl4cyFIjR2sZO9y+j6mOAQmZN29wABkUqdk83vqx0NGeixns3g52aPlIvF+Db2rOEPpKsE1lYlvdH/AjxC6i1Gajj5JeX49C+llcez/VrJGT66PiY4ROZE1DXp/6Yy3j0PuHBK7YjIAI521jWNOL/YcBK5xeWGPdD+xUDWYcDBA7jlhfoNkhrM7M2ncaGwDGGNnTGya7Da4WgGExwic9OsN9Cyv7JjZh0XlGrVvZ2D0MrPBTlF5Zi50YBEtbwYWP+2MhbJjaNnvcdIxie6zM/dkijHLw1sDVtrvmzXFZ8pInPU73+ilSNw+BcgeZfa0ZABbKyt8PLANnI8b1siUnOKb+wBdswE8tMA92Cg2wTjBElG98lfx1FcXonOIaLatb/a4WgKExwic+TXDogcpYz//A8bcWrU7W18Ed3MC2UVOnxQtYOmTgqylJ5T8kFeA2y540aLjmbkYemuZDn+z+C2svs81R0THCJzJTpF2zoBKXHAwaoaKKQp4gXt1SHhsujwin1p2JN0sW6fKLrLl+YpC87b32/sMMlIRf3e+PWw7E82uL0/uoR6qR2S5jDBITJXboFAr8nK+K9pLP6nUe2D3HFf5yA5fl284F2v+F/6fmDPt8p40HtsqKlRaw5nYvupC7CzscKUQW3VDkeT+JNPZM56TALcQ4C8VGDbZ2pHQwZ6cWBrONtZY39yDlbsS736HcWpyNVTxACIuBcI6d6QYVI9EbWP3v7tiByPv6UZgr2c1A5Jk5jgEJkzW0eg/xvKeNunQG6K2hGRAXxdHTDpdqU0/7t/HEVh6VUaqh5eAZzdBtg4Av24g06r5m09g6TsIvi62uPJPi3UDkezmOAQmbvw4UDTnkBFMbB2mtrRkIEe6xWKEC8nZOWXXnnbuNgWvqaqqF/PZwEP1kvRoqz8EnyxXinqJ3bRObOon8GY4BCZO7FCdeB0Zdu4aLiYtEPtiMgA9jbWcieNMHvLaSRnF9W+Q+wXQG4S4NZESXBIkz788xgKyyrRMdgDd3dqonY4msYEh8gSiAaLnR9Rxn+8DOh0akdEBhjQzg8xYd5y2/j0P5Q1GlJeGrDlI2UsTk3Zcc2GFh1MycWyeOU08tQ7w2FlxW3hN4MJDpGlEPVQ7N2A9H3A3oVqR0MGbhufOjQc4nXv94MZ2HrivHLDmleB8iIgOBpof5/aYZIBxO64qSsT5Drx4ZGBiGrKytM3iwkOkaVw8QVu+48yFmtxCqteHElT2ga4YXRMqBxP/SUBZcfXAQk/AY2sgEHvK6ckSXOW7k7G3qQcuVvuFW4LrxdMcIgsSdfxgF97oCSHC441bHL/VmjsYo+U8zkoXP7cpf/bwEi1QyMDXCgolbvjhMn9W8PfnZWn6wMTHCJLYm0D3Fm1VmPfd8DZWLUjIgO4OdjitTvbYrz1b/AsTkKlk49SuZo06b3VR2XH+Db+rng0pqna4ZgNJjhElia4G9B5tDL+bTJQWa52RGSAYSFleNZ2hRzPdXocerG+ijRn95ls/LBbWVj89t0Rsskq1Q8+k0SWSOy0cfQCsg4DO79SOxq6UXo9Gv3xEuxQhu26dpieEiFL+5O2lFfq8N/lCXI8smswopqy31R9YoJDZImcvIA7qiocb5gO5F6j/D+ZnqO/ASfWAFa2ONJZFPdrhNdXHkJR2VUqHJNJWrD9DI5l5sPTyVYW9SMNJTjZ2dkYNWoU3Nzc4OHhgXHjxqGgoOCq9z9z5ozcBnmlY9myZTX3u9LtS5YsMea3QmR+Ikcp24rLC4E/XlI7Gqqr0nyllpHQ8xk8NPgONPFwRFpuCT75S6mAS6YvNacYH689LsevDGoDT2c7tUMyO0ZNcERyc+jQIaxduxarVq3C5s2bMWHChKvePzg4GOnp6bWO119/HS4uLhg0aFCt+37zzTe17jd8+HBjfitE5kd0mR7yEWBlAxxdBRxS1nOQifvrf0BeCuDRFLjl33C0s8Ybd7WTN83dclo25CTTptfr8Z+fD8qKxaLezf1RbKuhqQTnyJEjWL16NebOnYvo6Gj06tULn3/+uZxpSUtLu+LnWFtbw9/fv9axfPlyPPDAAzLJuZyYEbr8fg4O3FZHdMP8I4Bezyvj3/8NFGWrHRFdy5ltwK65ynjYZzUVi/u29cOwjoHQ6YGXfzogKx2T6fp5Tyo2HT8HOxsrvHdvB1Ys1lqCExsbK5OQLl261FzXr18/WFlZYefOnXV6jPj4eOzbt0+e2vq7p556Co0bN0a3bt0wb948mRFfTWlpKfLy8modRFSl94uATxug8Byweora0dDViGaaK59WxmIXXFifWjdPGxoOL2c7HM3Ix5cbT6oTI9WpmeYbqw7L8bN9W6KFb+0376SBBCcjIwO+vr61rrOxsYGXl5e8rS6+/vprtG3bFj169Kh1/RtvvIEffvhBnvq699578eSTT8rZoauZPn063N3daw5xKoyIqtjYA8O+UJpxHlgCHF+jdkR0JRveAbJPAa4BQP+3/nGzt4s9/jdMOVU1Y8NJHMvIVyFIup5pvxySNW/aBbphQu8wtcMxazec4LzyyitXXQhcfRw9qlRkvBnFxcX4/vvvrzh789prr6Fnz57o1KkTXn75Zbz00kv44IMPrvpYU6ZMQW5ubs2RnJx80/ERmZXgrkD3J5XxqueAEs5ympTUeKVbuHDnx4CD+xXvNrRDAPq19UN5pR4v/bgfFZU8VWVK/jiYjj8SMmBj1Qjv39cBtqx5Y1Q3/Oy+8MILcn3NtY6wsDC5LiYrK6vW51ZUVMidVeK26/nxxx9RVFSE0aOrCpJdg1jjk5KSIk9FXYm9vb3cyXX5QUR/c/urgGcokJcK/MU2Diajogz45WlArwPa3w+0rr3h4nLiDaYoFufqYIP9KbmYty2xQUOlq8spKsNrvxyS44l9mqNd4JWTVKo/Njf6CT4+PvK4npiYGOTk5Mh1NFFRUfK69evXQ6fTyYSkLqenhg0bVqevJdbpeHp6ykSGiAwkFqwO+xxYMBTYPQ8Iv+sf6zxIBZs/ALIOAU6NgYHvXffufm4OeG1IOF766QD+b81x3N7Gj+s8TMD/Vh7C+YJStPR1waTbW6gdjkUw2vyYWDszcOBAjB8/HnFxcdi2bRsmTZqEkSNHIjAwUN4nNTUVbdq0kbdf7uTJk3JL+eOPP/6Px/3111/lzqyEhAR5v5kzZ+Kdd97B009XLb4jIsM16w10eUwZL58IFF9UOyLLlhwHbPlQGQ/+AHD2rtOn3d8lCLe0bIzSCh2eW7qXu6pUtnJ/GlbsS4PYLPXefR1gb2OtdkgWwagnABctWiQTmL59+2Lw4MFyq/js2bNrbi8vL8exY8fkqajLiV1RQUFB6N+//z8e09bWFjNmzJAzRJGRkZg1axY++ugjTJvGKXWieiEWsHo1B/LTgFXPy7YApFJBv5/HK6emOowAIu6p86eKU1Uf3t8RHk62SEjNwyd/KQXlqOGl5RTj1eUH5XjS7S3ROcRT7ZAsRiP9tfZXmymxTVzsphILjrkeh+gKUuKBr+8A9JXA3bOBjiPUjsjy/PIUsPc7wD0YmLjtqguLr7eodeKiPWjUCFg6IQbdmrHXUUPS6fQYNXcnYk9fQMdgD/z4RAwXFjfg6zefaSL6p6AooM+USwUAL55VOyLLcnilktyIrft3zzIouREGtQ/A/VFBchLu+aX7kFfCzvENae7W0zK5cbKzxicjIpncNDA+20R0ZaLCsehVVZoHLP8XoKtUOyLLkJcO/PqMMu71HBDa86YebtqwdgjxcpK9j0QNFmoYh9Py8MGfx+R46p3haNbYWe2QLA4THCK6MmsbZfbAzgVIigW2fqx2ROZPpwN+eVJZ3O3fAejzn5t+SBd7G3w8oqNc4Lp8b6pc8ErGVVJeKRd3i3pEd4T7YURXFpdVAxMcIro6r2bAoPcvVdIVvZDIeLb+H3BqPWDjANw7F7Cpnw7TUU29MOk2ZWuyaPJ4+lxBvTwuXdnUXxJwPLMAjV3s8e497eWib2p4THCI6NoiH1J28YgFxz+OBfIz1Y7IPJ3eqCSR1VvCfVrX68M/07cluoV6oaC0Ak8u2oPiMp5yNIYfdiXjh90pcsbs05GRsoUGqYMJDhFdm3j3KdoD+LQFCjKBn8YBlRVqR2Ve8tKAnx5XtoRHPqw006xnNtZW+OKhTnJWQTTk/O+Kg9dsUkyGrbt57ZcEOZ58Ryv0bNFY7ZAsGhMcIro+O2fggYXKepwzW4AN/2z2SAaqLAeWjVW6uftFAEOqCvsZga+bAz5/sJOcXfh5TyqW7mJfvvoidqg9uSheFle8rbUPnuzDasVqY4JDRHXj0woY9pkyFguOj/2hdkTm4a//Ack7AHs3JYm0dTTql4tp7o1/D1BOf01deQgJqblG/XqWQMyEvbhsP85cKEITD0d8PCISViKLJFUxwSGiuou4F+j2L2Usto5nn1Y7Im07/MulLuHDvwS8mzfIl32id3P0beMrWzhMXBQvG0GS4eZsOY0/D2XCztoKX47qDA+n+lkcTjeHCQ4R3Xgrh6CuQEku8P0IoDhH7Yi0KW0fsPwJZRwzCWg7tMG+tJhd+OiBSAR7OSI5uxgTv9vDflUGWnckE9P/OCrHr93ZVlYsJtPABIeIbozYuvzAt4BrIHD+OLDsUWUdCd3YouLFI4HyIqB5X6Df6w0egruTLWY/0gXOdtay2u5rKxK46NiARcVPL94rK0U/2C0YD3dvqnZIdBkmOER049wCgIeWALZOyvbm319kU866Ki1QZr7y0wGfNsD93yhFFVXQNsANXzzUWS46Xro7GbM385RjXWXllWDcgl0oKqtEzxbeeOOuCNa7MTFMcIjIMAEdgXu/VvolxX8D7PhS7YhMn2h38fMEIOMA4NQYeGipwX2m6sttbXzx2p3hcvzu6qP481CGqvFogagh9PjC3UjPLUGYjzO+fCiKfaZMEP9HiMhwbQYD/d9Uxn/+lzurruevacCx3wBre+DBxYBnKEzBmB6heKR7UzkJ99ySfTiYwp1V1+oQPvmHfTiQkgtPJ1t8M6arPN1HpocJDhHdHLFAtvOjYrMs8ONjwNlYtSMyTdu/ALZ/fmnHVHA3mApxamXa0HD0buWD4vJKjJ0fx3YOVyDWKL3+6yH8kZABW+tGmPVIFzT1ZhNNU8UEh4hujlh3MOT/gBb9lEWz3z8ApO1VOyrTsvsbYM1/lfHtrwHt74Opqa50LNblnC8ow8NzdyLlYpHaYZmU9/88hgWxZ+WP/If3d0S3Zl5qh0TXwASHiG6eta2ys6ppT6A0D/j2HiDriNpRmYYDPwCrnlfGPZ8FbnkBpsrNwRbfjuuG5j7OSMstwai5O+ViWgJmbDiJmRtPyfFbwyNwV2QTtUOi62CCQ0T1w84JeHAJENgZKM4GFt4FXFBeECzWkVVVtW70QNfHle3gJr7TRvSqWvR4d1kj5+yFIpnkZBdadiHAeVsT8cGfx+T4v4PbYlQ0t4NrARMcIqo/Dm7Awz8Bvu2UxpwiyclJgkU6uU7pvi66sHd8CBj0gcknN9X83R3w/ePd4e/mgBNZBRg9bydyiy2z1tHSXUl4Y9VhOX62b0uM7x2mdkhUR0xwiKh+OXkBo1cA3i2A3GRg3kDgnPLu16JaMIhCfpVlQNthwLDPRflgaEmwlxO+ezwa3s52SEjNw4hZscjKt6zTVV9vTcTLPx2U48d7NcNz/VqqHRLdAG39xhGRNrj4AqNXAo1bA3mpSpKTEg+LED8fWDZGSW7C71JqBalUyO9mtfB1waLx0fBxtcfRjHzcNzMWZy8UwhJ2S3345zG8WTVzM65XM/x3SFsW8tMYJjhEZBzuTYDHVgNNopQ1OQuGAqc2wGyJIjJb/g/49VlArwOixgL3faO0ttCwNv5u+OmJHgjxckJSdhHunRkrWxSYq0qdHv9dkYAvNpyUl18c0BqvMrnRJCY4RGTk01UrgbDbgPJCYNH9wKHlMDs6HbDmVWDdG8rlW/4N3PkxYGUNcxDi7YQfJ8ZUbSEvxYjZsYhLzIa5Ka2oxDOL9+L7nUmyfcU7d7fHU7e1YHKjUUxwiMi47F2UlgThwwFduXL6ZsM7StsCcyC6qS95EIj9Qrk84B2g72uaWVBcV76uDlgyoTu6hnoiv6RC1skRiYC5NOhMzy3GiFk78NvBdNhZW2HGQ53xUHSI2mHRTWCCQ0TGZ2MP3DcPiBZbpgFsek8pCFik8VmAjARgdh/g+Gql/cLds4GYp2Cu3B1tsfCxaAxs54+ySh3+s/wgXvrxAErKtZ2sbj91Hnd+thX7knPk9/jN2K4Y1D5A7bDoJjXSm0v6fQPy8vLg7u6O3NxcuLm5qR0OkWXZv0RZp1JRAng0BUZ8BwR0gOYcWAasfBqoKAbcQ4AR3wKBkbAE4mXjq02n8cGfR6HTAxFN3DBzVJTceaW170N0UH9vtfJ9hAe4YdYj2vs+LEneDbx+M8FhgkPU8NIPAEsfBnLOAjYOwIC3gajHtLGVurQAWDsV2C06qQNofruyU0qsN7IwW0+cx9OL9+BiUTk8nGzx7j3tMTBCGzMfYi3Rf5cfxJ+HMuXlezsH4e27I+Bgax7rpswVE5zrYIJDZALE6amfJwAn1yqXRZuHoZ8BjVvAZJ38C/j1eSA36dJi4tv+YzaLiQ2RmlOMid/Fy+7awqAIf7x+Vzu5ZscUiZe85XtTZfG+nKJy2TRz6tB2eDg6hIuJNYAJznUwwSEyod1HcbOU3UeiUadYx3LbFCDmadOqHSOSsT//A+xfrFz2CAGGfqrM3pDcffT5upP4atMpVOj0cHOwwat3huP+qCCTShpE89D/Lk/ApuPn5GVxSur9+zogoom72qFRHTHBuQ4mOEQm5uJZYNVzwKn1ymW/9kDfqUDLO9TdjVRRCuxZqCyKLhQvio2A7hOB2/6r7A6jWkR9nJd/OoCDqcpsTvcwL7w4oA2imnqqGld+STnmbzsjE7DCskrY2VjJtgsTeofB1loDp0WpBhOc62CCQ2SCxJ8iMUOyegpQkqNc16QL0GcK0KJvwyY6FWXA3m+Vwn2iErPg0wYY9gUQ3LXh4tCgikqdbHHw0drjKK3Qyet6t/LB8/1aolNIwyY6BaUVWLD9DOZsOS1PRwlim/u793ZAcx8mqFpkEgnO22+/jd9++w379u2DnZ0dcnKq/mBdgwhl2rRpmDNnjrx/z549MXPmTLRsean/R3Z2Np5++mn8+uuvsLKywr333otPP/0ULi51/2FlgkNkwgrPA9s+BeLmKDuUhKBuQI9JQKuBypZzo33tC0DCj8D2z5U+WoJrIHDLZKDzo5qvStyQkrOL8MX6k/hxT4qsDizc1toHY3o2Q68WjWEtKukZsabNT/EpMtESC6CFMB9nOWsztEMgrIz4tckCEhyRqHh4eCAlJQVff/11nRKc9957D9OnT8eCBQvQrFkzvPbaazh48CAOHz4MBwdlwdqgQYOQnp6OWbNmoby8HGPHjkXXrl3x/fff1zk2JjhEGlCQpSQ6u+YqW8oFB3eg3d1AhxFAcPf62XVVXqLUsTmwFDixBtBVKNe7+F9KbGxNc8GsFojeVZ+vP4mf96TIrdiC6G11V8dA3N25iVwHUx/rdMRpqD8SMrBibypiT1+QE4JCs8ZViU3HQKMmVWRBCU61+fPn47nnnrtugiPCCAwMxAsvvIB///vf8jrxDfj5+cnHGDlyJI4cOYLw8HDs2rULXbp0kfdZvXo1Bg8eLBMp8fl1wQSHSEPyM4CdXwEHfrh0ukhwCwJCugNBXZXDv33dZlhKcoHUPUDKbiBlF5C0AyhV1oxI/h2ATo8AnR8BbB2N8z1ZoMTzhZi3NRG/HkirOV0khDV2lmt0xOmrTiEeaOXnWqdEJLuwDPuSL2JvUo48dp/NRkm5ckpM6NbMCyO7BmNYx0DYcJ2N2dBkgnP69Gk0b94ce/fuRWTkpWJZt956q7wsTkPNmzdPJkAXL16sub2iokLO7ixbtgx33333FR+7tLRUHpc/QcHBwUxwiLREtHY4s1WZaTn8C1BWUPt2sQPLPUipR+PoCTh6AXZOSkJTfFHZCSWafuaIU09/+7Pn1gRofz/QcSTg27ZBvy1LU1ahw8ZjWVixLxV/Hc6SFZEv52RnjQB3B3g42cHTyRbujnawt7VCbnE5corKZHIkkpv03KpZvcuI01D3dGqCuyKbsFifmbqRBMdk9mFmZGTIj2LG5nLicvVt4qOvr2+t221sbODl5VVznysRp71ef/11o8RNRA1E1JoJu1U5Bn8IJMUCqfHKLIyYjRHJS/Yp5bgeUUE5qMul2Z/AztooMmgGxA6m/u385SGSll2J2bJFwt7ki9ifnCsXBp86VygWRF33sZr7ONfM/HQO8UQbf1eT2pZO6rqhBOeVV16R62SuRZxGatOmDUzJlClTMHny5H/M4BCRRomZGbGzShyCmIi+eAbIT6+aqbmoJDxlhYCDhzKjI2d2vADPpoBL7TdKpA7R96lfuJ88BLEYWZzKElWGxWyNWCAsZmxErytRKdnTyU5+FLM7zbyd4e5kq/a3QOaS4IjTQ2PGjLnmfcLCwgwKxN/fX37MzMxEQMClUt/icvUpK3GfrKysWp8nTlGJnVXVn38l9vb28iAiMyXetXs1Uw7SLLH2poWvizyIGjTB8fHxkYcxiF1TIklZt25dTUIjZlp27tyJiRMnyssxMTFyLU98fDyioqLkdevXr4dOp0N0dLRR4iIiIiLtMdpJ56SkJFkDR3ysrKyUY3EUFFxaGChOZS1fvlyOxXlTsRj5rbfewsqVK+X28NGjR8udUcOHD5f3adu2LQYOHIjx48cjLi4O27Ztw6RJk+QOq7ruoCIiIiLzZ7RFxlOnTpX1bKp16tRJftywYQP69Okjx8eOHZMroau99NJLKCwsxIQJE+RMTa9eveQ28OoaOMKiRYtkUtO3b9+aQn+fffaZsb4NIiIi0iC2auA2cSIiIrN7/ea+SCIiIjI7THCIiIjI7DDBISIiIrPDBIeIiIjMDhMcIiIiMjtMcIiIiMjsMMEhIiIis8MEh4iIiMwOExwiIiIyO0Zr1WDKqos3i4qIREREpA3Vr9t1acJgkQlOfn6+/BgcHKx2KERERGTA67ho2XAtFtmLSqfTIS0tDa6urrKLeX1nlyJxSk5OZp8rI+Nz3XD4XDccPtcNh8+19p5rkbKI5CYwMFA23L4Wi5zBEU9KUFCQUb+G+A/kL0zD4HPdcPhcNxw+1w2Hz7W2nuvrzdxU4yJjIiIiMjtMcIiIiMjsMMGpZ/b29pg2bZr8SMbF57rh8LluOHyuGw6fa/N+ri1ykTERERGZN87gEBERkdlhgkNERERmhwkOERERmR0mOERERGR2mODUoxkzZiA0NBQODg6Ijo5GXFyc2iFp3vTp09G1a1dZddrX1xfDhw/HsWPHat2npKQETz31FLy9veHi4oJ7770XmZmZqsVsLt59911Z6fu5556ruY7Pdf1JTU3Fww8/LJ9LR0dHtG/fHrt37665Xez/mDp1KgICAuTt/fr1w4kTJ1SNWYsqKyvx2muvoVmzZvJ5bN68Od58881avYz4XBtm8+bNGDp0qKwqLP5WrFixotbtdXles7OzMWrUKFn8z8PDA+PGjUNBQQHqhdhFRTdvyZIlejs7O/28efP0hw4d0o8fP17v4eGhz8zMVDs0TRswYID+m2++0SckJOj37dunHzx4sD4kJERfUFBQc58nnnhCHxwcrF+3bp1+9+7d+u7du+t79OihatxaFxcXpw8NDdV36NBB/+yzz9Zcz+e6fmRnZ+ubNm2qHzNmjH7nzp3606dP6//880/9yZMna+7z7rvv6t3d3fUrVqzQ79+/Xz9s2DB9s2bN9MXFxarGrjVvv/223tvbW79q1Sp9YmKiftmyZXoXFxf9p59+WnMfPteG+f333/X//e9/9T///LPIFvXLly+vdXtdnteBAwfqO3bsqN+xY4d+y5Yt+hYtWugffPBBfX1gglNPunXrpn/qqadqLldWVuoDAwP106dPVzUuc5OVlSV/kTZt2iQv5+Tk6G1tbeUfrWpHjhyR94mNjVUxUu3Kz8/Xt2zZUr927Vr9rbfeWpPg8LmuPy+//LK+V69eV71dp9Pp/f399R988EHNdeL5t7e31y9evLiBojQPQ4YM0T/22GO1rrvnnnv0o0aNkmM+1/Xj7wlOXZ7Xw4cPy8/btWtXzX3++OMPfaNGjfSpqak3HRNPUdWDsrIyxMfHy+m3y/tdicuxsbGqxmZucnNz5UcvLy/5UTzv5eXltZ77Nm3aICQkhM+9gcQpqCFDhtR6TgU+1/Vn5cqV6NKlC+6//3556rVTp06YM2dOze2JiYnIyMio9VyL/jvi1Def6xvTo0cPrFu3DsePH5eX9+/fj61bt2LQoEHyMp9r46jL8yo+itNS4nehmri/eP3cuXPnTcdgkc0269v58+fleV4/P79a14vLR48eVS0uc+wCL9aD9OzZExEREfI68QtkZ2cnf0n+/tyL2+jGLFmyBHv27MGuXbv+cRuf6/pz+vRpzJw5E5MnT8Z//vMf+Xw/88wz8vl99NFHa57PK/1N4XN9Y1555RXZyVok49bW1vJv9dtvvy3XfQh8ro2jLs+r+CgS/MvZ2NjIN7D18dwzwSFNzSwkJCTId19U/5KTk/Hss89i7dq1cqE8GTdZF+9a33nnHXlZzOCIn+2vvvpKJjhUf3744QcsWrQI33//Pdq1a4d9+/bJN0piYSyfa/PGU1T1oHHjxvKdwd93k4jL/v7+qsVlTiZNmoRVq1Zhw4YNCAoKqrlePL/iFGFOTk6t+/O5v3HiFFRWVhY6d+4s30WJY9OmTfjss8/kWLzz4nNdP8SukvDw8FrXtW3bFklJSXJc/Xzyb8rNe/HFF+UszsiRI+VOtUceeQTPP/+83KEp8Lk2jro8r+Kj+JtzuYqKCrmzqj6eeyY49UBMK0dFRcnzvJe/QxOXY2JiVI1N68TaNZHcLF++HOvXr5dbPS8nnndbW9taz73YRi5eKPjc35i+ffvi4MGD8h1u9SFmGcRUfvWYz3X9EKdZ/17uQKwRadq0qRyLn3PxB/7y51qcZhHrEvhc35iioiK5puNy4g2p+Bst8Lk2jro8r+KjeMMk3lxVE3/nxf+NWKtz0256mTLVbBMXq8Pnz58vV4ZPmDBBbhPPyMhQOzRNmzhxotxmuHHjRn16enrNUVRUVGvrstg6vn79erl1OSYmRh508y7fRSXwua6/bfg2NjZyC/OJEyf0ixYt0js5Oem/++67Wltsxd+QX375RX/gwAH9XXfdxa3LBnj00Uf1TZo0qdkmLrY0N27cWP/SSy/V3IfPteE7Lvfu3SsPkU589NFHcnz27Nk6P69im3inTp1kuYStW7fKHZzcJm6CPv/8c/nHX9TDEdvGxb5+ujnil+ZKh6iNU038sjz55JN6T09P+SJx9913yySI6j/B4XNdf3799Vd9RESEfGPUpk0b/ezZs2vdLrbZvvbaa3o/Pz95n759++qPHTumWrxalZeXJ3+Gxd9mBwcHfVhYmKzdUlpaWnMfPteG2bBhwxX/Pouksq7P64ULF2RCI2oTubm56ceOHSsTp/rQSPxz8/NARERERKaDa3CIiIjI7DDBISIiIrPDBIeIiIjMDhMcIiIiMjtMcIiIiMjsMMEhIiIis8MEh4iIiMwOExwiIiIyO0xwiIiIyOwwwSEiIiKzwwSHiIiIzA4THCIiIoK5+X+2r3y6CZCL2QAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "enc_t = TSCyclicalPosition()(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1] - 2\n",
    "plt.plot(enc_t[0, -2:].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t1 = torch.rand(bs, c_in, seq_len)\n",
    "t2 = torch.arange(seq_len)\n",
    "t2 = torch.cat([t2[35:], t2[:35]]).reshape(1, 1, -1)\n",
    "t = TSTensor(torch.cat([t1, t2], 1))\n",
    "mask = torch.rand_like(t) > .8\n",
    "t[mask] = np.nan\n",
    "enc_t = TSCyclicalPosition(3)(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1] - 2\n",
    "plt.plot(enc_t[0, -2:].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSLinearPosition(Transform):\n",
    "    \"Concatenates the position along the sequence as 1 additional variable\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self,\n",
    "        linear_var:int=None, # Optional variable to indicate the steps withing the cycle (ie minute of the day)\n",
    "        var_range:tuple=None, # Optional range indicating min and max values of the linear variable\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        drop_var:bool=False, # Flag to indicate if the cyclical var is removed\n",
    "        lin_range:tuple=(-1,1),\n",
    "        **kwargs):\n",
    "        self.linear_var, self.var_range, self.drop_var, self.lin_range = linear_var, var_range, drop_var, lin_range\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs,nvars,seq_len = o.shape\n",
    "        if self.linear_var is None:\n",
    "            lin = linear_encoding(seq_len, device=o.device, lin_range=self.lin_range)\n",
    "            output = torch.cat([o, lin.reshape(1,1,-1).repeat(bs,1,1)], 1)\n",
    "        else:\n",
    "            linear_var = o[:, [self.linear_var]]\n",
    "            if self.var_range is None:\n",
    "                lin = (linear_var - linear_var.min()) / (linear_var.max() - linear_var.min())\n",
    "            else:\n",
    "                lin = (linear_var - self.var_range[0]) / (self.var_range[1] - self.var_range[0])\n",
    "            lin = (linear_var - self.lin_range[0]) / (self.lin_range[1] - self.lin_range[0])\n",
    "            if self.drop_var:\n",
    "                exc_vars = np.isin(np.arange(nvars), self.linear_var, invert=True)\n",
    "                output = torch.cat([o[:, exc_vars], lin], 1)\n",
    "            else:\n",
    "                output = torch.cat([o, lin], 1)\n",
    "            return output\n",
    "        return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "enc_t = TSLinearPosition()(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1] - 1\n",
    "plt.plot(enc_t[0, -1].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "t = torch.arange(100)\n",
    "t1 = torch.cat([t[30:], t[:30]]).reshape(1, 1, -1)\n",
    "t2 = torch.cat([t[52:], t[:52]]).reshape(1, 1, -1)\n",
    "t = torch.cat([t1, t2]).float()\n",
    "mask = torch.rand_like(t) > .8\n",
    "t[mask] = np.nan\n",
    "t = TSTensor(t)\n",
    "enc_t = TSLinearPosition(linear_var=0, var_range=(0, 100), drop_var=True)(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1]\n",
    "plt.plot(enc_t[0, -1].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSMissingness(Transform):\n",
    "    \"Concatenates data missingness for selected features along the sequence as additional variables\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, feature_idxs=None, magnitude=None, **kwargs):\n",
    "        sel_vars = sel_vars or feature_idxs\n",
    "        self.sel_vars = listify(sel_vars)\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        if self.sel_vars is not None:\n",
    "            missingness = o[:, self.sel_vars].isnan()\n",
    "        else:\n",
    "            missingness = o.isnan()\n",
    "        return torch.cat([o, missingness], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "t[t>.5] = np.nan\n",
    "enc_t = TSMissingness(sel_vars=[0,2])(t)\n",
    "test_eq(enc_t.shape[1], 5)\n",
    "test_eq(enc_t[:, 3:], torch.isnan(t[:, [0,2]]).float())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSPositionGaps(Transform):\n",
    "    \"\"\"Concatenates gaps for selected features along the sequence as additional variables\"\"\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, feature_idxs=None, magnitude=None, forward=True, backward=False,\n",
    "                 nearest=False, normalize=True, **kwargs):\n",
    "        sel_vars = sel_vars or feature_idxs\n",
    "        self.sel_vars = listify(sel_vars)\n",
    "        self.gap_fn = partial(get_gaps, forward=forward, backward=backward, nearest=nearest, normalize=normalize)\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        if self.sel_vars:\n",
    "            gaps = self.gap_fn(o[:, self.sel_vars])\n",
    "        else:\n",
    "            gaps = self.gap_fn(o)\n",
    "        return torch.cat([o, gaps], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[   nan, 0.1228, 0.2583, 0.0637, 0.4105, 0.4971, 0.2206,    nan],\n",
       "         [   nan, 0.0705,    nan, 0.1024,    nan, 0.4173,    nan, 0.4713],\n",
       "         [0.0746, 0.0311,    nan, 0.0558,    nan, 0.4550,    nan,    nan],\n",
       "         [1.0000, 2.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 2.0000, 1.0000, 2.0000, 1.0000, 2.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 2.0000, 1.0000],\n",
       "         [1.0000, 2.0000, 1.0000, 2.0000, 1.0000, 3.0000, 2.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 2.0000, 1.0000, 2.0000, 1.0000, 1.0000]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,8\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "t[t>.5] = np.nan\n",
    "enc_t = TSPositionGaps(sel_vars=[0,2], forward=True, backward=True, nearest=True, normalize=False)(t)\n",
    "test_eq(enc_t.shape[1], 9)\n",
    "enc_t.data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSRollingMean(Transform):\n",
    "    \"\"\"Calculates the rolling mean for all/ selected features alongside the sequence\n",
    "\n",
    "       It replaces the original values or adds additional variables (default)\n",
    "       If nan values are found, they will be filled forward and backward\"\"\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, feature_idxs=None, magnitude=None, window=2, replace=False, **kwargs):\n",
    "        sel_vars = sel_vars or feature_idxs\n",
    "        self.sel_vars = listify(sel_vars)\n",
    "        self.rolling_mean_fn = partial(rolling_moving_average, window=window)\n",
    "        self.replace = replace\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        if self.sel_vars:\n",
    "            if torch.isnan(o[:, self.sel_vars]).any():\n",
    "                o[:, self.sel_vars] = fbfill_sequence(o[:, self.sel_vars])\n",
    "            rolling_mean = self.rolling_mean_fn(o[:, self.sel_vars])\n",
    "            if self.replace:\n",
    "                o[:, self.sel_vars] = rolling_mean\n",
    "                return o\n",
    "        else:\n",
    "            if torch.isnan(o).any():\n",
    "                o = fbfill_sequence(o)\n",
    "            rolling_mean = self.rolling_mean_fn(o)\n",
    "            if self.replace: return rolling_mean\n",
    "        return torch.cat([o, rolling_mean], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[   nan,    nan, 0.0485,    nan,    nan, 0.3877, 0.3619,    nan],\n",
      "         [0.3984, 0.4129,    nan, 0.5976, 0.2123,    nan, 0.2672, 0.2774],\n",
      "         [   nan, 0.3171,    nan, 0.3495, 0.3056,    nan, 0.5143, 0.1713]]])\n",
      "tensor([[[0.0485, 0.0485, 0.0485, 0.0485, 0.0485, 0.3877, 0.3619, 0.3619],\n",
      "         [0.3984, 0.4129,    nan, 0.5976, 0.2123,    nan, 0.2672, 0.2774],\n",
      "         [0.3171, 0.3171, 0.3171, 0.3495, 0.3056, 0.3056, 0.5143, 0.1713],\n",
      "         [0.0485, 0.0485, 0.0485, 0.0485, 0.0485, 0.1616, 0.2660, 0.3705],\n",
      "         [0.3171, 0.3171, 0.3171, 0.3279, 0.3240, 0.3202, 0.3751, 0.3304]]])\n",
      "tensor([[[0.0485, 0.0485, 0.0485, 0.0485, 0.0485, 0.1616, 0.2660, 0.3705],\n",
      "         [0.3984, 0.4057, 0.4081, 0.4745, 0.4076, 0.3407, 0.2306, 0.2523],\n",
      "         [0.3171, 0.3171, 0.3171, 0.3279, 0.3240, 0.3202, 0.3751, 0.3304]]])\n"
     ]
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,8\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "t[t > .6] = np.nan\n",
    "print(t.data)\n",
    "enc_t = TSRollingMean(sel_vars=[0,2], window=3)(t)\n",
    "test_eq(enc_t.shape[1], 5)\n",
    "print(enc_t.data)\n",
    "enc_t = TSRollingMean(window=3, replace=True)(t)\n",
    "test_eq(enc_t.shape[1], 3)\n",
    "print(enc_t.data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSLogReturn(Transform):\n",
    "    \"Calculates log-return of batch of type `TSTensor`. For positive values only\"\n",
    "    order = 90\n",
    "    def __init__(self, lag=1, pad=True, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.lag, self.pad = lag, pad\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch_diff(torch.log(o), lag=self.lag, pad=self.pad)\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(lag={self.lag}, pad={self.pad})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor([1,2,4,8,16,32,64,128,256]).float()\n",
    "test_eq(TSLogReturn(pad=False)(t).std(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSAdd(Transform):\n",
    "    \"Add a defined amount to each batch of type `TSTensor`.\"\n",
    "    order = 90\n",
    "    def __init__(self, add, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.add = add\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch.add(o, self.add)\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(lag={self.lag}, pad={self.pad})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor([1,2,3]).float()\n",
    "test_eq(TSAdd(1)(t), TSTensor([2,3,4]).float())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSClipByVar(Transform):\n",
    "    \"\"\"Clip  batch of type `TSTensor` by variable\n",
    "\n",
    "    Args:\n",
    "        var_min_max: list of tuples containing variable index, min value (or None) and max value (or None)\n",
    "    \"\"\"\n",
    "    order = 90\n",
    "    def __init__(self, var_min_max, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.var_min_max = var_min_max\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        for v,m,M in self.var_min_max:\n",
    "            o[:, v] = torch.clamp(o[:, v], m, M)\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.rand(16, 3, 10) * tensor([1,10,100]).reshape(1,-1,1))\n",
    "max_values = t.max(0).values.max(-1).values.data\n",
    "max_values2 = TSClipByVar([(1,None,5), (2,10,50)])(t).max(0).values.max(-1).values.data\n",
    "test_le(max_values2[1], 5)\n",
    "test_ge(max_values2[2], 10)\n",
    "test_le(max_values2[2], 50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSDropVars(Transform):\n",
    "    \"Drops selected variable from the input\"\n",
    "    order = 90\n",
    "    def __init__(self, drop_vars, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.drop_vars = drop_vars\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        exc_vars = np.isin(np.arange(o.shape[1]), self.drop_vars, invert=True)\n",
    "        return o[:, exc_vars]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[ 0,  1,  2,  3],\n",
       "         [ 4,  5,  6,  7]],\n",
       "\n",
       "        [[12, 13, 14, 15],\n",
       "         [16, 17, 18, 19]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = TSTensor(torch.arange(24).reshape(2, 3, 4))\n",
    "enc_t = TSDropVars(2)(t)\n",
    "test_ne(t, enc_t)\n",
    "enc_t.data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSOneHotEncode(Transform):\n",
    "    order = 90\n",
    "    def __init__(self,\n",
    "        sel_var:int, # Variable that is one-hot encoded\n",
    "        unique_labels:list, # List containing all labels (excluding nan values)\n",
    "        add_na:bool=False, # Flag to indicate if values not included in vocab should be set as 0\n",
    "        drop_var:bool=True, # Flag to indicate if the selected var is removed\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        **kwargs\n",
    "        ):\n",
    "        unique_labels = listify(unique_labels)\n",
    "        self.sel_var = sel_var\n",
    "        self.unique_labels = unique_labels\n",
    "        self.n_classes = len(unique_labels) + add_na\n",
    "        self.add_na = add_na\n",
    "        self.drop_var = drop_var\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs, n_vars, seq_len = o.shape\n",
    "        o_var = o[:, [self.sel_var]]\n",
    "        ohe_var = torch.zeros(bs, self.n_classes, seq_len, device=o.device)\n",
    "        if self.add_na:\n",
    "            is_na = torch.isin(o_var, o_var.new(list(self.unique_labels)), invert=True) # not available in dict\n",
    "            ohe_var[:, [0]] = is_na.to(ohe_var.dtype)\n",
    "        for i,l in enumerate(self.unique_labels):\n",
    "            ohe_var[:, [i + self.add_na]] = (o_var == l).to(ohe_var.dtype)\n",
    "        if self.drop_var:\n",
    "            exc_vars = torch.isin(torch.arange(o.shape[1], device=o.device), self.sel_var, invert=True)\n",
    "            output = torch.cat([o[:, exc_vars], ohe_var], 1)\n",
    "        else:\n",
    "            output = torch.cat([o, ohe_var], 1)\n",
    "        return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[1, 1, 0, 1, 2]],\n",
       "\n",
       "        [[2, 0, 2, 1, 2]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs = 2\n",
    "seq_len = 5\n",
    "t_cont = torch.rand(bs, 1, seq_len)\n",
    "t_cat = torch.randint(0, 3, t_cont.shape)\n",
    "t = TSTensor(torch.cat([t_cat, t_cont], 1))\n",
    "t_cat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[0., 0., 1., 0., 0.],\n",
       "         [1., 1., 0., 1., 0.],\n",
       "         [0., 0., 0., 0., 1.]],\n",
       "\n",
       "        [[0., 1., 0., 0., 0.],\n",
       "         [0., 0., 0., 1., 0.],\n",
       "         [1., 0., 1., 0., 1.]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tfm = TSOneHotEncode(0, [0, 1, 2])\n",
    "output = tfm(t)[:, -3:].data\n",
    "test_eq(t_cat, torch.argmax(tfm(t)[:, -3:], 1)[:, None])\n",
    "tfm(t)[:, -3:].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[10.,  5., 11., nan, 12.]],\n",
       "\n",
       "        [[ 5., 12., 10., nan, 11.]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs = 2\n",
    "seq_len = 5\n",
    "t_cont = torch.rand(bs, 1, seq_len)\n",
    "t_cat = torch.tensor([[10.,  5., 11., np.nan, 12.], [ 5., 12., 10., np.nan, 11.]])[:, None]\n",
    "t = TSTensor(torch.cat([t_cat, t_cont], 1))\n",
    "t_cat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[1., 0., 0., 0., 0.],\n",
       "         [0., 0., 1., 0., 0.],\n",
       "         [0., 0., 0., 0., 1.]],\n",
       "\n",
       "        [[0., 0., 1., 0., 0.],\n",
       "         [0., 0., 0., 0., 1.],\n",
       "         [0., 1., 0., 0., 0.]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tfm = TSOneHotEncode(0, [10, 11, 12], drop_var=False)\n",
    "mask = ~torch.isnan(t[:, 0])\n",
    "test_eq(tfm(t)[:, 0][mask], t[:, 0][mask])\n",
    "tfm(t)[:, -3:].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t1 = torch.randint(3, 7, (2, 1, 10))\n",
    "t2 = torch.rand(2, 1, 10)\n",
    "t = TSTensor(torch.cat([t1, t2], 1))\n",
    "output = TSOneHotEncode(0, [3, 4, 5], add_na=True, drop_var=True)(t)\n",
    "test_eq((t1 > 5).float(), output.data[:, [1]])\n",
    "test_eq((t1 == 3).float(), output.data[:, [2]])\n",
    "test_eq((t1 == 4).float(), output.data[:, [3]])\n",
    "test_eq((t1 == 5).float(), output.data[:, [4]])\n",
    "test_eq(output.shape, (t.shape[0], 5, t.shape[-1]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSPosition(Transform):\n",
    "    order = 90\n",
    "    def __init__(self,\n",
    "        steps:list, # List containing the steps passed as an additional variable. Theu should be normalized.\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        **kwargs\n",
    "        ):\n",
    "        self.steps = torch.from_numpy(np.asarray(steps)).reshape(1, 1, -1)\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs = o.shape[0]\n",
    "        steps = self.steps.expand(bs, -1, -1).to(device=o.device, dtype=o.dtype)\n",
    "        return torch.cat([o, steps], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(torch.float32, torch.float32)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = TSTensor(torch.rand(2, 1, 10)).float()\n",
    "a = np.linspace(-1, 1, 10).astype('float64')\n",
    "TSPosition(a)(t).data.dtype, t.dtype"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "import torch\n",
    "import torch.nn.functional as F\n",
    "\n",
    "class PatchEncoder():\n",
    "    \"Creates a sequence of patches from a 3d input tensor.\"\n",
    "\n",
    "    def __init__(self,\n",
    "        patch_len:int, # Number of time steps in each patch.\n",
    "        patch_stride:int=None, # Stride of the patch.\n",
    "        pad_at_start:bool=True, # If True, pad the input tensor at the start to ensure that the input tensor is evenly divisible by the patch length.\n",
    "        value:float=0.0, # Value to pad the input tensor with.\n",
    "        seq_len:int=None, # Number of time steps in the input tensor. If None, make sure seq_len >= patch_len and a multiple of stride\n",
    "        merge_dims:bool=True, # If True, merge channels within the same patch.\n",
    "        reduction:str='none', # type of reduction applied. Available: \"none\", \"mean\", \"min\", \"max\", \"mode\"\n",
    "        reduction_dim:int=-1, # dimension where the reduction is applied\n",
    "        swap_dims:tuple=None, # If True, swap the time and channel dimensions.\n",
    "        ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.seq_len = seq_len\n",
    "        self.patch_len = patch_len\n",
    "        self.patch_stride = patch_stride or patch_len\n",
    "        self.pad_at_start = pad_at_start\n",
    "        self.value = value\n",
    "        self.merge_dims = merge_dims\n",
    "\n",
    "        assert reduction in [\"none\", \"mean\", \"min\", \"max\", \"mode\"]\n",
    "        self.reduction = reduction\n",
    "        self.reduction_dim = reduction_dim\n",
    "        self.swap_dims = swap_dims\n",
    "\n",
    "        if seq_len is None:\n",
    "            self.pad_size = 0\n",
    "        elif self.seq_len < self.patch_len:\n",
    "            self.pad_size = self.patch_len - self.seq_len\n",
    "        else:\n",
    "            if (self.seq_len % self.patch_len) % self.patch_stride == 0:\n",
    "                self.pad_size = 0\n",
    "            else:\n",
    "                self.pad_size = self.patch_stride - (self.seq_len % self.patch_len) % self.patch_stride\n",
    "\n",
    "    def __call__(self,\n",
    "        x: torch.Tensor # 3d input tensor with shape [batch size, sequence length, channels]\n",
    "        ) -> torch.Tensor: #  Transformed tensor of patches with shape [batch size, channels*patch length, number of patches]\n",
    "\n",
    "        if x.ndim == 2:\n",
    "            x = x[:, None]\n",
    "\n",
    "        bs, c_in, *_ = x.size()\n",
    "        if not bs:\n",
    "            return x\n",
    "\n",
    "        if self.pad_size:\n",
    "            x = F.pad(x, (self.pad_size, 0), value=self.value) if self.pad_at_start else F.pad(x, (0, self.pad_size), value=self.value)\n",
    "\n",
    "        x = x.unfold(2, self.patch_len, self.patch_stride)\n",
    "        x = x.permute(0, 1, 3, 2)\n",
    "        if self.merge_dims:\n",
    "            x = x.reshape(bs, c_in * self.patch_len, -1)\n",
    "\n",
    "        if self.reduction == \"mean\":\n",
    "            x = x.mean(self.reduction_dim)\n",
    "        elif self.reduction == \"min\":\n",
    "            x = x.min(self.reduction_dim).values\n",
    "        elif self.reduction == \"max\":\n",
    "            x = x.max(self.reduction_dim).values\n",
    "        elif self.reduction == \"mode\":\n",
    "            x = x.mode(self.reduction_dim).values\n",
    "\n",
    "        if self.swap_dims:\n",
    "            x = x.swapaxes(*self.swap_dims)\n",
    "\n",
    "        return x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([3, 2, 17]) \n",
      "\n",
      "tensor([[[  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,\n",
      "           14,  15,  16],\n",
      "         [  0,  10,  20,  30,  40,  50,  60,  70,  80,  90, 100, 110, 120, 130,\n",
      "          140, 150, 160]],\n",
      "\n",
      "        [[  1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,\n",
      "           15,  16,  17],\n",
      "         [  1,  11,  21,  31,  41,  51,  61,  71,  81,  91, 101, 111, 121, 131,\n",
      "          141, 151, 161]],\n",
      "\n",
      "        [[  2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,\n",
      "           16,  17,  18],\n",
      "         [  2,  12,  22,  32,  42,  52,  62,  72,  82,  92, 102, 112, 122, 132,\n",
      "          142, 152, 162]]])\n",
      "torch.Size([3, 20, 3]) \n",
      "\n"
     ]
    }
   ],
   "source": [
    "seq_len = 17\n",
    "patch_len = 10\n",
    "patch_stride = 5\n",
    "\n",
    "z11 = torch.arange(seq_len).reshape(1, 1, -1)\n",
    "z12 = torch.arange(seq_len).reshape(1, 1, -1) * 10\n",
    "z1 = torch.cat((z11, z12), dim=1)\n",
    "z21 = torch.arange(seq_len).reshape(1, 1, -1)\n",
    "z22 = torch.arange(seq_len).reshape(1, 1, -1) * 10\n",
    "z2 = torch.cat((z21, z22), dim=1) + 1\n",
    "z31 = torch.arange(seq_len).reshape(1, 1, -1)\n",
    "z32 = torch.arange(seq_len).reshape(1, 1, -1) * 10\n",
    "z3 = torch.cat((z31, z32), dim=1) + 2\n",
    "z = torch.cat((z11, z21, z31), dim=0)\n",
    "z = torch.cat((z1, z2, z3), dim=0)\n",
    "print(z.shape, \"\\n\")\n",
    "print(z)\n",
    "\n",
    "patch_encoder = PatchEncoder(patch_len=patch_len, patch_stride=patch_stride, value=-1, seq_len=seq_len, merge_dims=True)\n",
    "output = patch_encoder(z)\n",
    "print(output.shape, \"\\n\")\n",
    "first_token = output[..., 0]\n",
    "expected_first_token = torch.tensor([[-1, -1, -1,  0,  1,  2,  3,  4,  5,  6, -1, -1, -1,  0, 10, 20, 30, 40,\n",
    "         50, 60],\n",
    "        [-1, -1, -1,  1,  2,  3,  4,  5,  6,  7, -1, -1, -1,  1, 11, 21, 31, 41,\n",
    "         51, 61],\n",
    "        [-1, -1, -1,  2,  3,  4,  5,  6,  7,  8, -1, -1, -1,  2, 12, 22, 32, 42,\n",
    "         52, 62]])\n",
    "test_eq(first_token, expected_first_token)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSPatchEncoder(Transform):\n",
    "    \"Tansforms a time series into a sequence of patches along the last dimension\"\n",
    "    order = 90\n",
    "\n",
    "    def __init__(self,\n",
    "        patch_len:int, # Number of time steps in each patch.\n",
    "        patch_stride:int=None, # Stride of the patch.\n",
    "        pad_at_start:bool=True, # If True, pad the input tensor at the start to ensure that the input tensor is evenly divisible by the patch length.\n",
    "        value:float=0.0, # Value to pad the input tensor with.\n",
    "        seq_len:int=None, # Number of time steps in the input tensor. If None, make sure seq_len >= patch_len and a multiple of stride\n",
    "        merge_dims:bool=True, # If True, merge channels within the same patch.\n",
    "        reduction:str='none', # type of reduction applied. Available: \"none\", \"mean\", \"min\", \"max\", \"mode\"\n",
    "        reduction_dim:int=-2, # dimension where the y reduction is applied.\n",
    "        swap_dims:tuple=None, # If True, swap the time and channel dimensions.\n",
    "        ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.patch_encoder = PatchEncoder(patch_len=patch_len,\n",
    "                                          patch_stride=patch_stride,\n",
    "                                          pad_at_start=pad_at_start,\n",
    "                                          value=value,\n",
    "                                          seq_len=seq_len,\n",
    "                                          merge_dims=merge_dims,\n",
    "                                          reduction=reduction,\n",
    "                                          reduction_dim=reduction_dim,\n",
    "                                          swap_dims=swap_dims)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return self.patch_encoder(o)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9]],\n",
      "\n",
      "        [[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]])\n",
      "torch.Size([2, 1, 10]) \n",
      "\n",
      "first patch:\n",
      " tensor([[ 0,  1,  2,  3],\n",
      "        [10, 11, 12, 13]]) \n",
      "\n",
      "first patch:\n",
      " tensor([[ 0,  0,  0,  1],\n",
      "        [ 0,  0, 10, 11]]) \n",
      "\n"
     ]
    }
   ],
   "source": [
    "bs = 2\n",
    "c_in = 1\n",
    "seq_len = 10\n",
    "patch_len = 4\n",
    "\n",
    "t = TSTensor(torch.arange(bs * c_in * seq_len).reshape(bs, c_in, seq_len))\n",
    "print(t.data)\n",
    "print(t.shape, \"\\n\")\n",
    "\n",
    "patch_encoder = TSPatchEncoder(patch_len=patch_len, patch_stride=1, seq_len=seq_len)\n",
    "output = patch_encoder(t)\n",
    "test_eq(output.shape, ([bs, patch_len, 7]))\n",
    "print(\"first patch:\\n\", output[..., 0].data, \"\\n\")\n",
    "\n",
    "patch_encoder = TSPatchEncoder(patch_len=patch_len, patch_stride=None, seq_len=seq_len)\n",
    "output = patch_encoder(t)\n",
    "test_eq(output.shape, ([bs, patch_len, 3]))\n",
    "print(\"first patch:\\n\", output[..., 0].data, \"\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSTuplePatchEncoder(ItemTransform):\n",
    "    \"Tansforms a time series with x and y into sequences of patches along the last dimension\"\n",
    "    order = 90\n",
    "\n",
    "    def __init__(self,\n",
    "        patch_len:int, # Number of time steps in each patch.\n",
    "        patch_stride:int=None, # Stride of the patch.\n",
    "        pad_at_start:bool=True, # If True, pad the input tensor at the start to ensure that the input tensor is evenly divisible by the patch length.\n",
    "        value:float=0.0, # Value to pad the input tensor with.\n",
    "        seq_len:int=None, # Number of time steps in the input tensor. If None, make sure seq_len >= patch_len and a multiple of stride\n",
    "        merge_dims:bool=True, # If True, merge y channels within the same patch.\n",
    "        reduction:str='none', # type of reduction applied to y. Available: \"none\", \"mean\", \"min\", \"max\", \"mode\"\n",
    "        reduction_dim:int=-2, # dimension where the y reduction is applied.\n",
    "        swap_dims:tuple=None, # If True, swap the time and channel dimensions in y.\n",
    "        ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.x_patch_encoder = PatchEncoder(patch_len=patch_len,\n",
    "                                            patch_stride=patch_stride,\n",
    "                                            pad_at_start=pad_at_start,\n",
    "                                            value=value,\n",
    "                                            seq_len=seq_len)\n",
    "\n",
    "        self.y_patch_encoder = PatchEncoder(patch_len=patch_len,\n",
    "                                            patch_stride=patch_stride,\n",
    "                                            pad_at_start=pad_at_start,\n",
    "                                            value=value,\n",
    "                                            seq_len=seq_len,\n",
    "                                            merge_dims=merge_dims,\n",
    "                                            reduction=reduction,\n",
    "                                            reduction_dim=reduction_dim,\n",
    "                                            swap_dims=swap_dims)\n",
    "\n",
    "    def encodes(self, xy):\n",
    "        if len(xy) == 2:\n",
    "            x, y = xy\n",
    "            x, y = self.x_patch_encoder(x), self.y_patch_encoder(y)\n",
    "            return (x, y)\n",
    "        elif len(xy) == 1:\n",
    "            x = xy[0]\n",
    "            x = self.x_patch_encoder(x)\n",
    "            return (x, )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],\n",
      "         [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]],\n",
      "\n",
      "        [[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],\n",
      "         [30, 31, 32, 33, 34, 35, 36, 37, 38, 39]]])\n",
      "tensor([[[  0,  10,  20,  30,  40,  50,  60,  70,  80,  90],\n",
      "         [100, 110, 120, 130, 140, 150, 160, 170, 180, 190]],\n",
      "\n",
      "        [[200, 210, 220, 230, 240, 250, 260, 270, 280, 290],\n",
      "         [300, 310, 320, 330, 340, 350, 360, 370, 380, 390]]])\n",
      "first x patch:\n",
      " tensor([[ 0,  1,  2,  3, 10, 11, 12, 13],\n",
      "        [20, 21, 22, 23, 30, 31, 32, 33]]) \n",
      "\n",
      "first y patch:\n",
      " tensor([[  0,  10,  20,  30, 100, 110, 120, 130],\n",
      "        [200, 210, 220, 230, 300, 310, 320, 330]]) \n",
      "\n",
      "first x patch:\n",
      " tensor([[ 0,  1,  2,  3, 10, 11, 12, 13],\n",
      "        [20, 21, 22, 23, 30, 31, 32, 33]]) \n",
      "\n",
      "first y patch:\n",
      " tensor([[ 30, 130],\n",
      "        [230, 330]]) \n",
      "\n"
     ]
    }
   ],
   "source": [
    "# test\n",
    "bs = 2\n",
    "c_in = 2\n",
    "seq_len = 10\n",
    "patch_len = 4\n",
    "\n",
    "x = torch.arange(bs * c_in * seq_len).reshape(bs, c_in, seq_len)\n",
    "y = torch.arange(bs * c_in * seq_len).reshape(bs, c_in, seq_len) * 10\n",
    "print(x)\n",
    "print(y)\n",
    "\n",
    "\n",
    "patch_encoder = TSTuplePatchEncoder(patch_len=patch_len, patch_stride=1, seq_len=seq_len, merge_dims=True)\n",
    "x_out, y_out = patch_encoder((x, y))\n",
    "test_eq(x_out.shape, ([bs, c_in * patch_len, 7]))\n",
    "test_eq(y_out.shape, ([bs, c_in * patch_len, 7]))\n",
    "print(\"first x patch:\\n\", x_out[..., 0].data, \"\\n\")\n",
    "print(\"first y patch:\\n\", y_out[..., 0].data, \"\\n\")\n",
    "\n",
    "patch_encoder = TSTuplePatchEncoder(patch_len=patch_len, patch_stride=1, seq_len=seq_len, merge_dims=False, reduction=\"max\")\n",
    "x_out, y_out = patch_encoder((x, y))\n",
    "test_eq(x_out.shape, ([bs, c_in * patch_len, 7]))\n",
    "test_eq(y_out.shape, ([bs, c_in, 7]))\n",
    "print(\"first x patch:\\n\", x_out[..., 0].data, \"\\n\")\n",
    "print(\"first y patch:\\n\", y_out[..., 0].data, \"\\n\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# sklearn API transforms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSShrinkDataFrame(BaseEstimator, TransformerMixin):\n",
    "    \"\"\"A transformer to shrink dataframe or series memory usage\"\"\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, # List[str], optional. Columns to shrink, all columns by default.\n",
    "        skip=None, # List[str], optional. Columns to skip, None by default.\n",
    "        obj2cat=True, # bool, optional. Convert object columns to category, True by default.\n",
    "        int2uint=False, # bool, optional. Convert int columns to uint, False by default.\n",
    "        verbose=True # bool, optional. Print memory usage info. True by default.\n",
    "        ):\n",
    "        self.columns, self.skip, self.obj2cat, self.int2uint, self.verbose = listify(columns), listify(skip), obj2cat, int2uint, verbose\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        if isinstance(X, pd.Series):\n",
    "            X = X.to_frame()\n",
    "        assert isinstance(X, pd.DataFrame), \"X must be a pd.DataFrame or pd.Series\"\n",
    "        if isinstance(X, pd.Series):\n",
    "            X = X.to_frame().apply(object2date)\n",
    "        else:\n",
    "            X = X.apply(object2date)\n",
    "        if self.columns:\n",
    "            self.dt = df_shrink_dtypes(X[self.columns], self.skip, obj2cat=self.obj2cat, int2uint=self.int2uint)\n",
    "        else:\n",
    "            self.dt = df_shrink_dtypes(X, self.skip, obj2cat=self.obj2cat, int2uint=self.int2uint)\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        if isinstance(X, pd.Series):\n",
    "            col_name = X.name\n",
    "            X = X.to_frame()\n",
    "        else:\n",
    "            col_name = None\n",
    "        assert isinstance(X, pd.DataFrame), \"X must be a pd.DataFrame or pd.Series\"\n",
    "        if self.verbose:\n",
    "            start_memory = X.memory_usage().sum()\n",
    "            print(f\"Initial memory usage: {bytes2str(start_memory):10}\")\n",
    "        if isinstance(X, pd.Series):\n",
    "            X = X.to_frame().apply(object2date)\n",
    "        else:\n",
    "            X = X.apply(object2date)\n",
    "        if self.columns:\n",
    "            X.loc[:, self.columns] = X[self.columns].astype(self.dt)\n",
    "        else:\n",
    "            X = X.astype(self.dt)\n",
    "        if self.verbose:\n",
    "            end_memory = X.memory_usage().sum()\n",
    "            print(f\"Final memory usage  : {bytes2str(end_memory):10} ({(end_memory - start_memory) / start_memory:.1%})\")\n",
    "        if col_name is not None:\n",
    "            X = X[col_name]\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X\n",
    "\n",
    "\n",
    "def object2date(x, format=None):\n",
    "    if not x.dtype == np.dtype('object'): return x\n",
    "    try:\n",
    "        return pd.to_datetime(x, format=format)\n",
    "    except:\n",
    "        return x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial memory usage: 292.00 B  \n",
      "Final memory usage  : 182.00 B   (-37.7%)\n"
     ]
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"ints64\"] = np.random.randint(0,3,10)\n",
    "df['floats64'] = np.random.rand(10)\n",
    "tfm = TSShrinkDataFrame()\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df[\"ints64\"].dtype, \"int8\")\n",
    "test_eq(df[\"floats64\"].dtype, \"float32\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial memory usage: 372.00 B  \n",
      "Final memory usage  : 262.00 B   (-29.6%)\n"
     ]
    }
   ],
   "source": [
    "# test with date\n",
    "df = pd.DataFrame()\n",
    "df[\"dates\"] = pd.date_range('1/1/2011', periods=10, freq='M').astype(str)\n",
    "df[\"ints64\"] = np.random.randint(0,3,10)\n",
    "df['floats64'] = np.random.rand(10)\n",
    "tfm = TSShrinkDataFrame()\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df[\"dates\"].dtype, \"datetime64[ns]\")\n",
    "test_eq(df[\"ints64\"].dtype, \"int8\")\n",
    "test_eq(df[\"floats64\"].dtype, \"float32\")\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial memory usage: 212.00 B  \n",
      "Final memory usage  : 212.00 B   (0.0%)\n"
     ]
    }
   ],
   "source": [
    "# test with date and series\n",
    "df = pd.DataFrame()\n",
    "df[\"dates\"] = pd.date_range('1/1/2011', periods=10, freq='M').astype(str)\n",
    "tfm = TSShrinkDataFrame()\n",
    "df = tfm.fit_transform(df[\"dates\"])\n",
    "test_eq(df.dtype, \"datetime64[ns]\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSOneHotEncoder(BaseEstimator, TransformerMixin):\n",
    "    \"Encode categorical variables using one-hot encoding\"\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        columns=None,  # (str or List[str], optional): Column name(s) to encode. If None, all columns will be encoded. Defaults to None.\n",
    "        drop=True,  # (bool, optional): Whether to drop the original columns after encoding. Defaults to True.\n",
    "        add_na=True,  # (bool, optional): Whether to add a 'NaN' category for missing values. Defaults to True.\n",
    "        dtype=np.int8,  # (type, optional): Data type of the encoded output. Defaults to np.int64.\n",
    "    ):\n",
    "        self.columns = listify(columns)\n",
    "        self.drop, self.add_na, self.dtype = drop, add_na, dtype\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        handle_unknown = \"ignore\" if self.add_na else \"error\"\n",
    "        self.ohe_tfm = sklearn.preprocessing.OneHotEncoder(handle_unknown=handle_unknown)\n",
    "        self.dtypes = [X[c].dtype for c in self.columns]\n",
    "        if len(self.columns) == 1:\n",
    "            self.ohe_tfm.fit(X[self.columns].to_numpy().reshape(-1, 1))\n",
    "        else:\n",
    "            self.ohe_tfm.fit(X[self.columns])\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if len(self.columns) == 1:\n",
    "            output = self.ohe_tfm.transform(X[self.columns].to_numpy().reshape(-1, 1)).toarray().astype(self.dtype)\n",
    "        else:\n",
    "            output = self.ohe_tfm.transform(X[self.columns]).toarray().astype(self.dtype)\n",
    "        new_cols = []\n",
    "        for i,col in enumerate(self.columns):\n",
    "            for cats in self.ohe_tfm.categories_[i]:\n",
    "                new_cols.append(f\"{str(col)}_{str(cats)}\")\n",
    "        X[new_cols] = output\n",
    "        self.new_cols = new_cols\n",
    "        if self.drop: X = X.drop(self.columns, axis=1)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        if len(self.new_cols) == 1:\n",
    "            output = self.ohe_tfm.inverse_transform(X[self.new_cols].to_numpy().reshape(-1, 1))\n",
    "        else:\n",
    "            output = self.ohe_tfm.inverse_transform(X[self.new_cols])\n",
    "        for i,(col,d) in enumerate(zip(self.columns, self.dtypes)):\n",
    "            X[col] = output[:, i]\n",
    "            if hasattr(d, \"categories\"):\n",
    "                X[col] = X[col].astype('category')\n",
    "        if self.drop:\n",
    "            X = X.drop(self.new_cols, axis=1)\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = np.random.randint(0,2,10)\n",
    "df[\"b\"] = np.random.randint(0,3,10)\n",
    "unique_cols = len(df[\"a\"].unique()) + len(df[\"b\"].unique())\n",
    "tfm = TSOneHotEncoder()\n",
    "tfm.fit(df)\n",
    "df = tfm.transform(df)\n",
    "test_eq(df.shape[1], unique_cols)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSCategoricalEncoder(BaseEstimator, TransformerMixin):\n",
    "    \"\"\"A transformer to encode categorical columns\"\"\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, # List[str], optional. Columns to encode, all columns by default.\n",
    "        add_na=True, # bool, optional. Add a NaN category, True by default.\n",
    "        sort=True, # bool, optional. Sort categories by frequency, True by default.\n",
    "        categories='auto', # dict, optional. The custom mapping of categories. 'auto' by default.\n",
    "        inplace=True, # bool, optional. Modify input DataFrame, True by default.\n",
    "        prefix=None, # str, optional. Prefix for created column names. None by default.\n",
    "        suffix=None, # str, optional. Suffix for created column names. None by default.\n",
    "        drop=False # bool, optional. Drop original columns, False by default.\n",
    "        ):\n",
    "        self.columns = listify(columns)\n",
    "        self.add_na = add_na\n",
    "        self.prefix = prefix\n",
    "        self.suffix = suffix\n",
    "        self.sort = sort\n",
    "        self.inplace = inplace\n",
    "        self.drop = drop\n",
    "        if categories is None or categories == 'auto':\n",
    "            self.categories = None\n",
    "        else:\n",
    "            assert is_listy(categories) and len(categories) > 0, \"you must pass a list or list of lists of categories\"\n",
    "            self.categories = self.to_categorical(categories)\n",
    "\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        if self.categories is None:\n",
    "            _categories = []\n",
    "            for column in self.columns:\n",
    "                if isinstance(X, pd.DataFrame) and hasattr(X[column], \"cat\"):\n",
    "                    categories = X[column].cat.categories\n",
    "                elif hasattr(X, \"cat\"):\n",
    "                    categories = X.cat.categories\n",
    "                else:\n",
    "                    categories = X.loc[idxs, column].dropna().unique() if isinstance(X, pd.DataFrame) else X[idxs].dropna().unique()\n",
    "                    if self.sort:\n",
    "                        categories = np.sort(categories)\n",
    "                categories = pd.CategoricalDtype(categories=categories, ordered=True)\n",
    "                _categories.append(categories)\n",
    "            self.categories = _categories\n",
    "        assert len(self.categories) == len(self.columns)\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if isinstance(X, pd.DataFrame):\n",
    "            columns = X.columns\n",
    "        else:\n",
    "            columns = X.name\n",
    "        for column, categories in zip(self.columns, self.categories):\n",
    "            if column not in columns:\n",
    "                continue\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                name = []\n",
    "                if self.prefix: name += [self.prefix]\n",
    "                name += [column]\n",
    "                if self.suffix: name += [self.suffix]\n",
    "                new_col = '_'.join(name)\n",
    "                if self.drop:\n",
    "                    X.loc[:, column] = X.loc[:, column].astype(categories).cat.codes + self.add_na\n",
    "                    X.rename(columns={column: new_col}, inplace=True)\n",
    "                else:\n",
    "                    X.loc[:, new_col] = X.loc[:, column].astype(categories).cat.codes + self.add_na\n",
    "            else:\n",
    "                X = X.astype(categories).cat.codes + self.add_na\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if isinstance(X, pd.DataFrame):\n",
    "            columns = X.columns\n",
    "            for column, categories in zip(self.columns, self.categories):\n",
    "                if column not in columns:\n",
    "                    continue\n",
    "                name = []\n",
    "                if self.prefix: name += [self.prefix]\n",
    "                name += [column]\n",
    "                if self.suffix: name += [self.suffix]\n",
    "                new_col = '_'.join(name)\n",
    "                if self.add_na:\n",
    "                    X.loc[:, new_col] = np.array(['#na#'] + list(categories.categories))[X.loc[:, new_col].astype(int)]\n",
    "                else:\n",
    "                    X.loc[:, new_col] = categories.categories[X.loc[:, new_col].astype(int)]\n",
    "        else:\n",
    "            if self.add_na:\n",
    "                X = pd.Series(np.array(['#na#'] + list(self.categories[0].categories))[X], name=X.name, index=X.index)\n",
    "            else:\n",
    "                X = pd.Series(self.categories[0].categories[X], name=X.name, index=X.index)\n",
    "        return X\n",
    "\n",
    "    def to_categorical(self, categories):\n",
    "        if is_listy(categories[0]):\n",
    "            return [pd.CategoricalDtype(categories=np.sort(c) if self.sort else c, ordered=True) for c in categories]\n",
    "        else:\n",
    "            return pd.CategoricalDtype(categories=np.sort(categories) if self.sort else categories, ordered=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Stateful transforms like TSCategoricalEncoder can easily be serialized. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import joblib"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   a  B\n",
       "1   a  A\n",
       "2   b  A\n",
       "3   a  C\n",
       "4   a  A\n",
       ".. .. ..\n",
       "95  b  B\n",
       "96  b  C\n",
       "97  a  B\n",
       "98  a  A\n",
       "99  a  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>1</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>2</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   0  2\n",
       "1   0  1\n",
       "2   2  1\n",
       "3   1  3\n",
       "4   1  1\n",
       ".. .. ..\n",
       "95  2  2\n",
       "96  2  3\n",
       "97  1  2\n",
       "98  1  1\n",
       "99  1  2\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>#na#</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "       a  b\n",
       "0   #na#  B\n",
       "1   #na#  A\n",
       "2      b  A\n",
       "3      a  C\n",
       "4      a  A\n",
       "..   ... ..\n",
       "95     b  B\n",
       "96     b  C\n",
       "97     a  B\n",
       "98     a  A\n",
       "99     a  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = alphabet[np.random.randint(0,2,100)]\n",
    "df[\"b\"] = ALPHABET[np.random.randint(0,3,100)]\n",
    "display(df)\n",
    "a_unique = len(df[\"a\"].unique())\n",
    "b_unique = len(df[\"b\"].unique())\n",
    "tfm = TSCategoricalEncoder()\n",
    "tfm.fit(df, idxs=slice(0, 50))\n",
    "joblib.dump(tfm, \"data/TSCategoricalEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSCategoricalEncoder.joblib\")\n",
    "df.loc[0, \"a\"] = 'z'\n",
    "df.loc[1, \"a\"] = 'h'\n",
    "df = tfm.transform(df)\n",
    "display(df)\n",
    "test_eq(df['a'].max(), a_unique)\n",
    "test_eq(df['b'].max(), b_unique)\n",
    "df = tfm.inverse_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   a  A\n",
       "1   a  C\n",
       "2   a  A\n",
       "3   a  A\n",
       "4   b  A\n",
       ".. .. ..\n",
       "95  b  C\n",
       "96  a  C\n",
       "97  a  B\n",
       "98  b  A\n",
       "99  b  C\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>d</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>e</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>e</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>c</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   d  B\n",
       "1   e  A\n",
       "2   a  A\n",
       "3   b  B\n",
       "4   e  C\n",
       ".. .. ..\n",
       "95  a  A\n",
       "96  b  C\n",
       "97  b  C\n",
       "98  b  C\n",
       "99  c  A\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   0  2\n",
       "1   0  1\n",
       "2   1  1\n",
       "3   2  2\n",
       "4   0  3\n",
       ".. .. ..\n",
       "95  1  1\n",
       "96  2  3\n",
       "97  2  3\n",
       "98  2  3\n",
       "99  0  1\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>#na#</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>#na#</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "       a  b\n",
       "0   #na#  B\n",
       "1   #na#  A\n",
       "2      a  A\n",
       "3      b  B\n",
       "4   #na#  C\n",
       "..   ... ..\n",
       "95     a  A\n",
       "96     b  C\n",
       "97     b  C\n",
       "98     b  C\n",
       "99  #na#  A\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = alphabet[np.random.randint(0,2,100)]\n",
    "df[\"a\"] = df[\"a\"].astype('category')\n",
    "df[\"b\"] = ALPHABET[np.random.randint(0,3,100)]\n",
    "display(df)\n",
    "a_unique = len(df[\"a\"].unique())\n",
    "b_unique = len(df[\"b\"].unique())\n",
    "tfm = TSCategoricalEncoder()\n",
    "tfm.fit(df)\n",
    "joblib.dump(tfm, \"data/TSCategoricalEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSCategoricalEncoder.joblib\")\n",
    "df[\"a\"] = alphabet[np.random.randint(0,5,100)]\n",
    "df[\"a\"] = df[\"a\"].astype('category')\n",
    "df[\"b\"] = ALPHABET[np.random.randint(0,3,100)]\n",
    "display(df)\n",
    "df = tfm.transform(df)\n",
    "display(df)\n",
    "test_eq(df['a'].max(), a_unique)\n",
    "test_eq(df['b'].max(), b_unique)\n",
    "df = tfm.inverse_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0     a\n",
       "1     b\n",
       "2     b\n",
       "3     b\n",
       "4     a\n",
       "     ..\n",
       "95    b\n",
       "96    b\n",
       "97    b\n",
       "98    b\n",
       "99    b\n",
       "Name: a, Length: 100, dtype: category\n",
       "Categories (2, object): ['a', 'b']"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "0     1\n",
       "1     2\n",
       "2     2\n",
       "3     2\n",
       "4     1\n",
       "     ..\n",
       "95    2\n",
       "96    2\n",
       "97    2\n",
       "98    2\n",
       "99    2\n",
       "Length: 100, dtype: int8"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "0     a\n",
       "1     b\n",
       "2     b\n",
       "3     b\n",
       "4     a\n",
       "     ..\n",
       "95    b\n",
       "96    b\n",
       "97    b\n",
       "98    b\n",
       "99    b\n",
       "Length: 100, dtype: object"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = alphabet[np.random.randint(0,2,100)]\n",
    "df[\"a\"] = df[\"a\"].astype('category')\n",
    "s = df['a']\n",
    "display(s)\n",
    "tfm = TSCategoricalEncoder()\n",
    "tfm.fit(s)\n",
    "joblib.dump(tfm, \"data/TSCategoricalEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSCategoricalEncoder.joblib\")\n",
    "s = tfm.transform(s)\n",
    "display(s)\n",
    "s = tfm.inverse_transform(s)\n",
    "display(s)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSTargetEncoder(TransformerMixin, BaseEstimator):\n",
    "    def __init__(self,\n",
    "        target_column, # column containing the target\n",
    "        columns=None, # List[str], optional. Columns to encode, all non-numerical columns by default.\n",
    "        inplace=True, # bool, optional. Modify input DataFrame, True by default.\n",
    "        prefix=None, # str, optional. Prefix for created column names. None by default.\n",
    "        suffix=None, # str, optional. Suffix for created column names. None by default.\n",
    "        drop=True, # bool, optional. Drop original columns, False by default.\n",
    "        dtypes=[\"object\", \"category\"], # List[str]. List with dtypes that will be used to identify columns to encode if not explicitly passed.\n",
    "        ):\n",
    "        \"Transforms categorical columns into numerical by replacing categories with target means.\"\n",
    "\n",
    "        self.columns = listify(columns)\n",
    "        self.target_column = target_column\n",
    "        self.target_means = {}\n",
    "        self.inplace = inplace\n",
    "        self.prefix = prefix\n",
    "        self.suffix = suffix\n",
    "        self.drop = drop\n",
    "        self.dtypes = listify(dtypes)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns:\n",
    "            self.columns = X.select_dtypes(include=self.dtypes).columns\n",
    "\n",
    "        assert self.target_column in X.columns\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        X_fit = X.loc[idxs]\n",
    "\n",
    "        for column in self.columns:\n",
    "            assert column in X.columns\n",
    "            self.target_means[column] = X_fit.groupby(column)[self.target_column].mean()\n",
    "\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.inplace:\n",
    "            X = X.copy()\n",
    "        for column in self.columns:\n",
    "            if column not in X.columns:\n",
    "                continue\n",
    "            name = []\n",
    "            if self.prefix: name += [self.prefix]\n",
    "            name += [column]\n",
    "            if self.suffix: name += [self.suffix]\n",
    "            new_col = '_'.join(name)\n",
    "            if self.drop:\n",
    "                X.loc[:, column] = X.loc[:, column].map(self.target_means[column])\n",
    "                X.rename(columns={column: new_col}, inplace=True)\n",
    "            else:\n",
    "                X.loc[:, new_col] = X.loc[:, column].map(self.target_means[column])\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        raise NotImplementedError(\"This method cannot be implemented because the original data cannot be reconstructed exactly.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>category1</th>\n",
       "      <th>category2</th>\n",
       "      <th>continuous</th>\n",
       "      <th>target</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.896091</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.318003</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.110052</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>large</td>\n",
       "      <td>0.227935</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.427108</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.325400</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.746491</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.649633</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.849223</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.657613</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 4 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "   category1 category2  continuous  target\n",
       "0     rabbit     small    0.896091       0\n",
       "1        cat     small    0.318003       1\n",
       "2     rabbit     small    0.110052       1\n",
       "3     rabbit     large    0.227935       0\n",
       "4        cat     large    0.427108       0\n",
       "..       ...       ...         ...     ...\n",
       "95       cat     small    0.325400       0\n",
       "96       cat     large    0.746491       0\n",
       "97    rabbit     small    0.649633       1\n",
       "98       cat     small    0.849223       0\n",
       "99       cat     large    0.657613       1\n",
       "\n",
       "[100 rows x 4 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(80,)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>category1</th>\n",
       "      <th>category2</th>\n",
       "      <th>continuous</th>\n",
       "      <th>target</th>\n",
       "      <th>category1_te</th>\n",
       "      <th>category2_te</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.896091</td>\n",
       "      <td>0</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.318003</td>\n",
       "      <td>1</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.110052</td>\n",
       "      <td>1</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>large</td>\n",
       "      <td>0.227935</td>\n",
       "      <td>0</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.427108</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.325400</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.746491</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.649633</td>\n",
       "      <td>1</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.849223</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.657613</td>\n",
       "      <td>1</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 6 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "   category1 category2  continuous  target  category1_te  category2_te\n",
       "0     rabbit     small    0.896091       0      0.565217      0.500000\n",
       "1        cat     small    0.318003       1      0.555556      0.500000\n",
       "2     rabbit     small    0.110052       1      0.565217      0.500000\n",
       "3     rabbit     large    0.227935       0      0.565217      0.521739\n",
       "4        cat     large    0.427108       0      0.555556      0.521739\n",
       "..       ...       ...         ...     ...           ...           ...\n",
       "95       cat     small    0.325400       0      0.555556      0.500000\n",
       "96       cat     large    0.746491       0      0.555556      0.521739\n",
       "97    rabbit     small    0.649633       1      0.565217      0.500000\n",
       "98       cat     small    0.849223       0      0.555556      0.500000\n",
       "99       cat     large    0.657613       1      0.555556      0.521739\n",
       "\n",
       "[100 rows x 6 columns]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# Create a dataframe with 100 rows\n",
    "np.random.seed(42)\n",
    "df = pd.DataFrame({\n",
    "    'category1': np.random.choice(['cat', 'dog', 'rabbit'], 100),\n",
    "    'category2': np.random.choice(['large', 'small'], 100),\n",
    "    'continuous': np.random.rand(100),\n",
    "    'target': np.random.randint(0, 2, 100)\n",
    "})\n",
    "\n",
    "display(df)\n",
    "\n",
    "# Split the data into train and test sets\n",
    "train_idx, test_idx = train_test_split(np.arange(100), test_size=0.2, random_state=42)\n",
    "print(train_idx.shape)\n",
    "\n",
    "# Initialize the encoder\n",
    "encoder = TSTargetEncoder(columns=['category1', 'category2'], target_column='target', inplace=False, suffix=\"te\", drop=False)\n",
    "\n",
    "# Fit the encoder using the training data\n",
    "encoder.fit(df, idxs=train_idx)\n",
    "\n",
    "# Transform the whole dataframe\n",
    "df_encoded = encoder.transform(df)\n",
    "\n",
    "# Check the results\n",
    "for c in [\"category1\", \"category2\"]:\n",
    "    for v in df[c].unique():\n",
    "        assert df.loc[train_idx][df.loc[train_idx, c] == v][\"target\"].mean() == df_encoded[df_encoded[c] == v][f\"{c}_te\"].mean()\n",
    "\n",
    "df_encoded"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "default_date_attr = ['Year', 'Month', 'Week', 'Day', 'Dayofweek', 'Dayofyear', 'Is_month_end', 'Is_month_start',\n",
    "                     'Is_quarter_end', 'Is_quarter_start', 'Is_year_end', 'Is_year_start']\n",
    "\n",
    "class TSDateTimeEncoder(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, datetime_columns=None, prefix=None, drop=True, time=False, attr=default_date_attr):\n",
    "        self.datetime_columns = listify(datetime_columns)\n",
    "        self.prefix, self.drop, self.time, self.attr = prefix, drop, time, listify(attr)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if self.time: self.attr = self.attr + ['Hour', 'Minute', 'Second']\n",
    "        if not self.datetime_columns:\n",
    "            self.datetime_columns = X.columns\n",
    "        self.prefixes = []\n",
    "        for dt_column in self.datetime_columns:\n",
    "            self.prefixes.append(re.sub('[Dd]ate$', '', dt_column) if self.prefix is None else self.prefix)\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "\n",
    "        for dt_column,prefix in zip(self.datetime_columns,self.prefixes):\n",
    "            make_date(X, dt_column)\n",
    "            field = X[dt_column]\n",
    "\n",
    "            # Pandas removed `dt.week` in v1.1.10\n",
    "            week = field.dt.isocalendar().week.astype(field.dt.day.dtype) if hasattr(field.dt, 'isocalendar') else field.dt.week\n",
    "            for n in self.attr: X[prefix + \"_\" + n] = getattr(field.dt, n.lower()) if n != 'Week' else week\n",
    "            if self.drop: X = X.drop(self.datetime_columns, axis=1)\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime as dt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>_Year</th>\n",
       "      <th>_Month</th>\n",
       "      <th>_Week</th>\n",
       "      <th>_Day</th>\n",
       "      <th>_Dayofweek</th>\n",
       "      <th>_Dayofyear</th>\n",
       "      <th>_Is_month_end</th>\n",
       "      <th>_Is_month_start</th>\n",
       "      <th>_Is_quarter_end</th>\n",
       "      <th>_Is_quarter_start</th>\n",
       "      <th>_Is_year_end</th>\n",
       "      <th>_Is_year_start</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2025</td>\n",
       "      <td>7</td>\n",
       "      <td>31</td>\n",
       "      <td>29</td>\n",
       "      <td>1</td>\n",
       "      <td>210</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2025</td>\n",
       "      <td>7</td>\n",
       "      <td>31</td>\n",
       "      <td>30</td>\n",
       "      <td>2</td>\n",
       "      <td>211</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   _Year  _Month  _Week  _Day  _Dayofweek  _Dayofyear  _Is_month_end  \\\n",
       "0   2025       7     31    29           1         210          False   \n",
       "1   2025       7     31    30           2         211          False   \n",
       "\n",
       "   _Is_month_start  _Is_quarter_end  _Is_quarter_start  _Is_year_end  \\\n",
       "0            False            False              False         False   \n",
       "1            False            False              False         False   \n",
       "\n",
       "   _Is_year_start  \n",
       "0           False  \n",
       "1           False  "
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df.loc[0, \"date\"] = dt.datetime.now()\n",
    "df.loc[1, \"date\"] = dt.datetime.now() + pd.Timedelta(1, unit=\"D\")\n",
    "tfm = TSDateTimeEncoder()\n",
    "joblib.dump(tfm, \"data/TSDateTimeEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSDateTimeEncoder.joblib\")\n",
    "tfm.fit_transform(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSDropIfTrueCols(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, columns=None):\n",
    "        self.columns = listify(columns)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        mask = X[self.columns].sum(axis=1) == 0\n",
    "        X = X[mask].reset_index(drop=True)\n",
    "        X.drop(columns=self.columns, inplace=True)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        raise NotImplementedError(\"Inverse transform is not implemented for TSDropIfTrueCols\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(None,)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# test TSDropIfTrueCols\n",
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 0, 1, 0, 0]\n",
    "df[\"b\"] = [0, 0, 0, 0, 0]\n",
    "df[\"c\"] = [0, 1, 0, 0, 1]\n",
    "\n",
    "expected_output = pd.DataFrame()\n",
    "expected_output[\"b\"] = [0, 0, 0, 0]\n",
    "expected_output[\"c\"] = [0, 1, 0, 1]\n",
    "\n",
    "tfm = TSDropIfTrueCols(\"a\")\n",
    "output = tfm.fit_transform(df)\n",
    "test_eq(output, expected_output),\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSApplyFunction(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, function, groups=None, group_keys=False, axis=1, columns=None, reset_index=False, drop=True):\n",
    "        self.function = function\n",
    "        self.groups = listify(groups)\n",
    "        self.group_keys = group_keys\n",
    "        self.columns = listify(columns)\n",
    "        self.reset_index = reset_index\n",
    "        self.drop = drop\n",
    "        self.axis = axis\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if self.columns is None:\n",
    "            self.columns = X.columns\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if self.groups:\n",
    "            if self.columns:\n",
    "                X = X.groupby(self.groups, group_keys=self.group_keys)[self.columns].apply(lambda x: self.function(x))\n",
    "            else:\n",
    "                X = X.groupby(self.groups, group_keys=self.group_keys).apply(lambda x: self.function(x))\n",
    "        else:\n",
    "            if self.columns:\n",
    "                X[self.columns] = X[self.columns].apply(lambda x: self.function(x), axis=self.axis)\n",
    "            else:\n",
    "                X = X.apply(lambda x: self.function(x), axis=self.axis)\n",
    "        if self.reset_index:\n",
    "            X = X.reset_index(drop=self.drop)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        raise NotImplementedError(\"Inverse transform is not implemented for ApplyFunction\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "a    1\n",
       "b    1\n",
       "c    1\n",
       "dtype: int64"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 0, 1, 0, 0]\n",
    "df[\"b\"] = [0, 0, 0, 0, 0]\n",
    "df[\"c\"] = [0, 1, 0, 0, 1]\n",
    "\n",
    "df.apply(lambda x: 1, )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# test ApplyFunction without groups\n",
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 0, 1, 0, 0]\n",
    "df[\"b\"] = [0, 0, 0, 0, 0]\n",
    "df[\"c\"] = [0, 1, 0, 0, 1]\n",
    "\n",
    "expected_output = pd.Series([1,1,1])\n",
    "\n",
    "tfm = TSApplyFunction(lambda x: 1, axis=0, reset_index=True)\n",
    "output = tfm.fit_transform(df)\n",
    "test_eq(output, expected_output)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# test ApplyFunction with groups and square function\n",
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 1, 2, 3, 4]\n",
    "df[\"id\"] = [0, 0, 0, 1, 1]\n",
    "\n",
    "expected_output = pd.Series([5, 25])\n",
    "\n",
    "tfm = TSApplyFunction(lambda x: (x[\"a\"]**2).sum(), groups=\"id\")\n",
    "\n",
    "output = tfm.fit_transform(df)\n",
    "test_eq(output, expected_output)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSMissingnessEncoder(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, columns=None):\n",
    "        self.columns = listify(columns)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        self.missing_columns = [f\"{cn}_missing\" for cn in self.columns]\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        X[self.missing_columns] = X[self.columns].isnull().astype(int)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        X.drop(self.missing_columns, axis=1, inplace=True)\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>a_missing</th>\n",
       "      <th>b_missing</th>\n",
       "      <th>c_missing</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.511342</td>\n",
       "      <td>0.501516</td>\n",
       "      <td>0.798295</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.649964</td>\n",
       "      <td>0.701967</td>\n",
       "      <td>0.795793</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.337995</td>\n",
       "      <td>0.375583</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.093982</td>\n",
       "      <td>0.578280</td>\n",
       "      <td>0.035942</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.465598</td>\n",
       "      <td>0.542645</td>\n",
       "      <td>0.286541</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.590833</td>\n",
       "      <td>0.030500</td>\n",
       "      <td>0.037348</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.360191</td>\n",
       "      <td>0.127061</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.522243</td>\n",
       "      <td>0.769994</td>\n",
       "      <td>0.215821</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.622890</td>\n",
       "      <td>0.085347</td>\n",
       "      <td>0.051682</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c  a_missing  b_missing  c_missing\n",
       "0       NaN       NaN       NaN          1          1          1\n",
       "1  0.511342  0.501516  0.798295          0          0          0\n",
       "2  0.649964  0.701967  0.795793          0          0          0\n",
       "3       NaN  0.337995  0.375583          1          0          0\n",
       "4  0.093982  0.578280  0.035942          0          0          0\n",
       "5  0.465598  0.542645  0.286541          0          0          0\n",
       "6  0.590833  0.030500  0.037348          0          0          0\n",
       "7       NaN  0.360191  0.127061          1          0          0\n",
       "8  0.522243  0.769994  0.215821          0          0          0\n",
       "9  0.622890  0.085347  0.051682          0          0          0"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data = np.random.rand(10,3)\n",
    "data[data > .8] = np.nan\n",
    "df = pd.DataFrame(data, columns=[\"a\", \"b\", \"c\"])\n",
    "tfm = TSMissingnessEncoder()\n",
    "tfm.fit(df)\n",
    "joblib.dump(tfm, \"data/TSMissingnessEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSMissingnessEncoder.joblib\")\n",
    "df = tfm.transform(df)\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSSortByColumns(TransformerMixin, BaseEstimator):\n",
    "    \"Transforms a dataframe by sorting by columns.\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns, # Columns to sort by\n",
    "        ascending=True, # Ascending or descending\n",
    "        inplace=True, # Perform operation in place\n",
    "        kind='stable', # Type of sort to use\n",
    "        na_position='last', # Where to place NaNs\n",
    "        ignore_index=False, # Do not preserve index\n",
    "        key=None, # Function to apply to values before sorting\n",
    "        ):\n",
    "        self.columns, self.ascending, self.inplace, self.kind, self.na_position, self.ignore_index, self.key = \\\n",
    "        listify(columns), ascending, inplace, kind, na_position, ignore_index, key\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.inplace:\n",
    "            X.sort_values(self.columns, axis=0, ascending=self.ascending, inplace=True, kind=self.kind,\n",
    "                          na_position=self.na_position, ignore_index=self.ignore_index, key=self.key)\n",
    "        else:\n",
    "            X = X.sort_values(self.columns, axis=0, ascending=self.ascending, inplace=False, kind=self.kind,\n",
    "                              na_position=self.na_position, ignore_index=self.ignore_index, key=self.key)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df_ori = df.copy()\n",
    "tfm = TSSortByColumns([\"a\", \"b\"])\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df_ori.sort_values([\"a\", \"b\"]).values, df.values)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSSelectColumns(TransformerMixin, BaseEstimator):\n",
    "    \"Transform used to select columns\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns # str or List[str]. Selected columns.\n",
    "        ):\n",
    "        self.columns = listify(columns)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, idxs=None, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if idxs is not None:\n",
    "            return X.loc[idxs, self.columns]\n",
    "        return X[self.columns].copy()\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df_ori = df.copy()\n",
    "tfm = TSSelectColumns([\"a\", \"b\"])\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df_ori[[\"a\", \"b\"]].values, df.values)\n",
    "df = tfm.inverse_transform(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "PD_TIME_UNITS = dict([\n",
    "    (\"Y\", \"year\"),\n",
    "    (\"M\", \"month\"),\n",
    "    (\"W\", \"week\"),\n",
    "    (\"D\", \"day\"),\n",
    "    (\"h\", \"hour\"),\n",
    "    (\"m\", \"minute\"),\n",
    "    (\"s\", \"second\"),\n",
    "    (\"ms\", \"millisecond\"),\n",
    "    (\"us\", \"microsecond\"),\n",
    "    (\"ns\", \"nanosecond\"),\n",
    "    (\"ps\", \"picosecond\"),\n",
    "    (\"fs\", \"femtosecond\"),\n",
    "    (\"as\", \"attosecond\")\n",
    "])\n",
    "\n",
    "class TSStepsSinceStart(BaseEstimator, TransformerMixin):\n",
    "    \"Add a column indicating the number of steps since the start in each row\"\n",
    "\n",
    "    def __init__(self,\n",
    "        datetime_col, # (str or List[str]): Column name(s) containing datetime values.\n",
    "        datetime_unit=\"D\", #(str, optional): Time unit of the datetime values. Defaults to 'D'.\n",
    "        start_datetime=None, #(str or pd.Timestamp, optional): The start datetime value. If None, the minimum value of the datetime_col is used. Defaults to None.\n",
    "        drop=False, # (bool, optional): Whether to drop the datetime_col column after computing time steps. Defaults to False.\n",
    "        dtype=None, # (type, optional): Data type of the time steps. Defaults to None.\n",
    "):\n",
    "\n",
    "        self.datetime_col = listify(datetime_col)[0]\n",
    "        self.datetime_div = np.timedelta64(1, datetime_unit)\n",
    "        datetime_value = PD_TIME_UNITS[datetime_unit]\n",
    "        self.drop = drop\n",
    "        self.dtype = dtype\n",
    "        self.new_col = f\"{datetime_value}s_since_start\"\n",
    "        self.datetime_unit = datetime_unit\n",
    "        if start_datetime is None:\n",
    "            self.start_datetime = None\n",
    "        elif isinstance(start_datetime, Timestamp):\n",
    "            self.start_datetime = start_datetime\n",
    "        else:\n",
    "            self.start_datetime = Timestamp(start_datetime)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.start_datetime is None:\n",
    "            self.start_datetime = X[self.datetime_col].min()\n",
    "        self.ori_dtype = X[self.datetime_col].dtypes\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        time_deltas = (pd.to_timedelta(X[self.datetime_col] - self.start_datetime) / self.datetime_div).astype(dtype=self.dtype)\n",
    "        if self.drop:\n",
    "            X[self.datetime_col] = time_deltas\n",
    "            X.rename(columns={self.datetime_col:self.new_col}, inplace=True)\n",
    "        else:\n",
    "            X[self.new_col] = time_deltas\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        datetimes  = pd.to_datetime(X[self.new_col] * self.datetime_div + self.start_datetime).astype(dtype=self.ori_dtype)\n",
    "        if self.drop:\n",
    "            X[self.new_col] = datetimes\n",
    "            X.rename(columns={self.new_col:self.datetime_col}, inplace=True)\n",
    "        else:\n",
    "            X[self.datetime_col] = datetimes\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.643288</td>\n",
       "      <td>0.458253</td>\n",
       "      <td>0.545617</td>\n",
       "      <td>2020-01-01</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.941465</td>\n",
       "      <td>0.386103</td>\n",
       "      <td>0.961191</td>\n",
       "      <td>2020-01-02</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.905351</td>\n",
       "      <td>0.195791</td>\n",
       "      <td>0.069361</td>\n",
       "      <td>2020-01-03</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.100778</td>\n",
       "      <td>0.018222</td>\n",
       "      <td>0.094443</td>\n",
       "      <td>2020-01-04</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.683007</td>\n",
       "      <td>0.071189</td>\n",
       "      <td>0.318976</td>\n",
       "      <td>2020-01-05</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.844875</td>\n",
       "      <td>0.023272</td>\n",
       "      <td>0.814468</td>\n",
       "      <td>2020-01-06</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.281855</td>\n",
       "      <td>0.118165</td>\n",
       "      <td>0.696737</td>\n",
       "      <td>2020-01-07</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.628943</td>\n",
       "      <td>0.877472</td>\n",
       "      <td>0.735071</td>\n",
       "      <td>2020-01-08</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.803481</td>\n",
       "      <td>0.282035</td>\n",
       "      <td>0.177440</td>\n",
       "      <td>2020-01-09</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.750615</td>\n",
       "      <td>0.806835</td>\n",
       "      <td>0.990505</td>\n",
       "      <td>2020-01-10</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime\n",
       "0  0.643288  0.458253  0.545617 2020-01-01\n",
       "1  0.941465  0.386103  0.961191 2020-01-02\n",
       "2  0.905351  0.195791  0.069361 2020-01-03\n",
       "3  0.100778  0.018222  0.094443 2020-01-04\n",
       "4  0.683007  0.071189  0.318976 2020-01-05\n",
       "5  0.844875  0.023272  0.814468 2020-01-06\n",
       "6  0.281855  0.118165  0.696737 2020-01-07\n",
       "7  0.628943  0.877472  0.735071 2020-01-08\n",
       "8  0.803481  0.282035  0.177440 2020-01-09\n",
       "9  0.750615  0.806835  0.990505 2020-01-10"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>days_since_start</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.643288</td>\n",
       "      <td>0.458253</td>\n",
       "      <td>0.545617</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.941465</td>\n",
       "      <td>0.386103</td>\n",
       "      <td>0.961191</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.905351</td>\n",
       "      <td>0.195791</td>\n",
       "      <td>0.069361</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.100778</td>\n",
       "      <td>0.018222</td>\n",
       "      <td>0.094443</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.683007</td>\n",
       "      <td>0.071189</td>\n",
       "      <td>0.318976</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.844875</td>\n",
       "      <td>0.023272</td>\n",
       "      <td>0.814468</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.281855</td>\n",
       "      <td>0.118165</td>\n",
       "      <td>0.696737</td>\n",
       "      <td>6</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.628943</td>\n",
       "      <td>0.877472</td>\n",
       "      <td>0.735071</td>\n",
       "      <td>7</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.803481</td>\n",
       "      <td>0.282035</td>\n",
       "      <td>0.177440</td>\n",
       "      <td>8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.750615</td>\n",
       "      <td>0.806835</td>\n",
       "      <td>0.990505</td>\n",
       "      <td>9</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c  days_since_start\n",
       "0  0.643288  0.458253  0.545617                 0\n",
       "1  0.941465  0.386103  0.961191                 1\n",
       "2  0.905351  0.195791  0.069361                 2\n",
       "3  0.100778  0.018222  0.094443                 3\n",
       "4  0.683007  0.071189  0.318976                 4\n",
       "5  0.844875  0.023272  0.814468                 5\n",
       "6  0.281855  0.118165  0.696737                 6\n",
       "7  0.628943  0.877472  0.735071                 7\n",
       "8  0.803481  0.282035  0.177440                 8\n",
       "9  0.750615  0.806835  0.990505                 9"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=10)\n",
    "display(df)\n",
    "df_ori = df.copy()\n",
    "tfm = TSStepsSinceStart(\"datetime\", datetime_unit=\"D\", drop=True, dtype=np.int32)\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)\n",
    "test_eq(df[\"days_since_start\"].values, np.arange(10))\n",
    "df = tfm.inverse_transform(df)\n",
    "test_eq(df_ori.values, df.values)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSStandardScaler(TransformerMixin, BaseEstimator):\n",
    "    \"Scale the values of specified columns in the input DataFrame to have a mean of 0 and standard deviation of 1.\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, # Column name(s) to be transformed. If None, all columns are transformed. Defaults to None.\n",
    "        mean=None, # Mean value for each column. If None, the mean value of each column is calculated during the fit method. Defaults to None.\n",
    "        std=None, # Stdev value for each column. If None, the standard deviation value of each column is calculated during the fit method. Defaults to None.\n",
    "        eps=1e-6, # A small value to avoid division by zero. Defaults to 1e-6.\n",
    "    ):\n",
    "        self.columns = listify(columns)\n",
    "        self.mean = mean\n",
    "        self.std = std\n",
    "        self.eps = np.array(eps, dtype='float32')\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        if self.mean is None:\n",
    "            self.mean = []\n",
    "            for c in self.columns:\n",
    "                self.mean.append(X.loc[idxs, c].mean())\n",
    "        else:\n",
    "            assert len(self.mean) == len(self.columns)\n",
    "        if self.std is None:\n",
    "            self.std = []\n",
    "            for c in self.columns:\n",
    "                self.std.append(X.loc[idxs, c].std())\n",
    "        else:\n",
    "            assert len(self.std) == len(self.columns)\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, s in zip(self.columns, self.mean, self.std):\n",
    "            X[c] = (X[c] - m) / (s + self.eps)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, s in zip(self.columns, self.mean, self.std):\n",
    "            X[c] = X[c] * (s + self.eps)  + m\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(100,3), columns=[\"a\", \"b\", \"c\"])\n",
    "tfm = TSStandardScaler()\n",
    "df = tfm.fit_transform(df)\n",
    "test_close(df.mean().values, np.zeros(3), 1e-3)\n",
    "test_close(df.std().values, np.ones(3), 1e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(1000,3), columns=[\"a\", \"b\", \"c\"])\n",
    "tfm = TSStandardScaler()\n",
    "df = tfm.fit_transform(df, idxs=slice(0, 800))\n",
    "test_close(df.mean().values, np.zeros(3), 1e-1)\n",
    "test_close(df.std().values, np.ones(3), 1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSRobustScaler(TransformerMixin, BaseEstimator):\n",
    "    \"\"\"This Scaler removes the median and scales the data according to the quantile range (defaults to IQR: Interquartile Range)\"\"\"\n",
    "\n",
    "    def __init__(self, columns=None, quantile_range=(25.0, 75.0), eps=1e-6):\n",
    "        self.columns = listify(columns)\n",
    "        self.quantile_range = quantile_range\n",
    "        self.eps = np.array(eps, dtype='float32')\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "\n",
    "        self.median = []\n",
    "        for c in self.columns:\n",
    "            self.median.append(np.nanpercentile(X.loc[idxs, c], 50))\n",
    "\n",
    "        self.iqr = []\n",
    "        for c in self.columns:\n",
    "            q1 = np.nanpercentile(X.loc[idxs, c], self.quantile_range[0])\n",
    "            q3 = np.nanpercentile(X.loc[idxs, c], self.quantile_range[1])\n",
    "            self.iqr.append(q3 - q1)\n",
    "\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, q in zip(self.columns, self.median, self.iqr):\n",
    "            X[c] = (X[c] - m) / (q + self.eps)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, q in zip(self.columns, self.median, self.iqr):\n",
    "            X[c] = X[c] * (q + self.eps) + m\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# test RobustScaler\n",
    "df = pd.DataFrame(np.random.rand(100,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"a\"] = df[\"a\"] * 100\n",
    "df[\"b\"] = df[\"b\"] * 10\n",
    "tfm = TSRobustScaler()\n",
    "df = tfm.fit_transform(df)\n",
    "test_close(df.median().values, np.zeros(3), 1e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSAddMissingTimestamps(TransformerMixin, BaseEstimator):\n",
    "    def __init__(self, datetime_col=None, use_index=False, unique_id_cols=None, fill_value=np.nan, range_by_group=True,\n",
    "                 start_date=None, end_date=None, freq=None):\n",
    "        assert datetime_col is not None or use_index\n",
    "        store_attr()\n",
    "        self.func = partial(add_missing_timestamps, datetime_col=datetime_col, use_index=use_index, unique_id_cols=unique_id_cols,\n",
    "                            fill_value=fill_value, range_by_group=range_by_group, start_date=start_date, end_date=end_date,\n",
    "                            freq=freq)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        X = self.func(X)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.211126</td>\n",
       "      <td>0.752468</td>\n",
       "      <td>0.051294</td>\n",
       "      <td>2020-01-01</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.394572</td>\n",
       "      <td>0.529941</td>\n",
       "      <td>0.161367</td>\n",
       "      <td>2020-01-03</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.571996</td>\n",
       "      <td>0.805432</td>\n",
       "      <td>0.760161</td>\n",
       "      <td>2020-01-04</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.361075</td>\n",
       "      <td>0.408456</td>\n",
       "      <td>0.679697</td>\n",
       "      <td>2020-01-06</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.056680</td>\n",
       "      <td>0.034673</td>\n",
       "      <td>0.391911</td>\n",
       "      <td>2020-01-07</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.259828</td>\n",
       "      <td>0.886086</td>\n",
       "      <td>0.895690</td>\n",
       "      <td>2020-01-09</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.297287</td>\n",
       "      <td>0.229994</td>\n",
       "      <td>0.411304</td>\n",
       "      <td>2020-01-10</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime\n",
       "0  0.211126  0.752468  0.051294 2020-01-01\n",
       "2  0.394572  0.529941  0.161367 2020-01-03\n",
       "3  0.571996  0.805432  0.760161 2020-01-04\n",
       "5  0.361075  0.408456  0.679697 2020-01-06\n",
       "6  0.056680  0.034673  0.391911 2020-01-07\n",
       "8  0.259828  0.886086  0.895690 2020-01-09\n",
       "9  0.297287  0.229994  0.411304 2020-01-10"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>datetime</th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0.211126</td>\n",
       "      <td>0.752468</td>\n",
       "      <td>0.051294</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2020-01-02</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0.394572</td>\n",
       "      <td>0.529941</td>\n",
       "      <td>0.161367</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0.571996</td>\n",
       "      <td>0.805432</td>\n",
       "      <td>0.760161</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2020-01-05</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0.361075</td>\n",
       "      <td>0.408456</td>\n",
       "      <td>0.679697</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0.056680</td>\n",
       "      <td>0.034673</td>\n",
       "      <td>0.391911</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2020-01-08</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0.259828</td>\n",
       "      <td>0.886086</td>\n",
       "      <td>0.895690</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>0.297287</td>\n",
       "      <td>0.229994</td>\n",
       "      <td>0.411304</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    datetime         a         b         c\n",
       "0 2020-01-01  0.211126  0.752468  0.051294\n",
       "1 2020-01-02       NaN       NaN       NaN\n",
       "2 2020-01-03  0.394572  0.529941  0.161367\n",
       "3 2020-01-04  0.571996  0.805432  0.760161\n",
       "4 2020-01-05       NaN       NaN       NaN\n",
       "5 2020-01-06  0.361075  0.408456  0.679697\n",
       "6 2020-01-07  0.056680  0.034673  0.391911\n",
       "7 2020-01-08       NaN       NaN       NaN\n",
       "8 2020-01-09  0.259828  0.886086  0.895690\n",
       "9 2020-01-10  0.297287  0.229994  0.411304"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=10)\n",
    "df = df.iloc[[0, 2, 3, 5, 6, 8, 9]]\n",
    "display(df)\n",
    "tfm = TSAddMissingTimestamps(datetime_col=\"datetime\", freq=\"D\")\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)\n",
    "test_eq(df.shape[0], 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        date  id  feature1  feature2\n",
       "0 2021-05-03   0  0.826065  0.793818\n",
       "1 2021-05-05   0  0.824350  0.577807\n",
       "2 2021-05-06   0  0.396992  0.866102\n",
       "3 2021-05-07   0  0.156317  0.289440\n",
       "4 2021-05-01   1  0.737951  0.467681\n",
       "5 2021-05-03   1  0.671271  0.411190\n",
       "6 2021-05-04   1  0.270644  0.427486\n",
       "7 2021-05-06   1  0.992582  0.564232"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-02</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         date  id  feature1  feature2\n",
       "0  2021-05-03   0  0.826065  0.793818\n",
       "1  2021-05-04   0       NaN       NaN\n",
       "2  2021-05-05   0  0.824350  0.577807\n",
       "3  2021-05-06   0  0.396992  0.866102\n",
       "4  2021-05-07   0  0.156317  0.289440\n",
       "5  2021-05-01   1  0.737951  0.467681\n",
       "6  2021-05-02   1       NaN       NaN\n",
       "7  2021-05-03   1  0.671271  0.411190\n",
       "8  2021-05-04   1  0.270644  0.427486\n",
       "9  2021-05-05   1       NaN       NaN\n",
       "10 2021-05-06   1  0.992582  0.564232"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "# Filling dates between min and max dates for each value in groupby column\n",
    "dates = pd.date_range('2021-05-01', '2021-05-07').values\n",
    "dates = np.concatenate((dates, dates))\n",
    "data = np.zeros((len(dates), 4))\n",
    "data[:, 0] = dates\n",
    "data[:, 1] = np.array([0]*(len(dates)//2)+[1]*(len(dates)//2))\n",
    "data[:, 2] = np.random.rand(len(dates))\n",
    "data[:, 3] = np.random.rand(len(dates))\n",
    "cols = ['date', 'id', 'feature1', 'feature2']\n",
    "date_df = pd.DataFrame(data, columns=cols).astype({'date': 'datetime64[ns]', 'id': int, 'feature1': float, 'feature2': float})\n",
    "date_df_with_missing_dates = date_df.drop([0,1,3,8,11,13]).reset_index(drop=True)\n",
    "display(date_df_with_missing_dates)\n",
    "tfm = TSAddMissingTimestamps(datetime_col=\"date\", unique_id_cols=\"id\", freq=\"D\")\n",
    "df = tfm.fit_transform(date_df_with_missing_dates.copy())\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        date  id  feature1  feature2\n",
       "0 2021-05-03   0  0.826065  0.793818\n",
       "1 2021-05-05   0  0.824350  0.577807\n",
       "2 2021-05-06   0  0.396992  0.866102\n",
       "3 2021-05-07   0  0.156317  0.289440\n",
       "4 2021-05-01   1  0.737951  0.467681\n",
       "5 2021-05-03   1  0.671271  0.411190\n",
       "6 2021-05-04   1  0.270644  0.427486\n",
       "7 2021-05-06   1  0.992582  0.564232"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-02</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2021-05-02</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         date  id  feature1  feature2\n",
       "0  2021-05-01   0       NaN       NaN\n",
       "1  2021-05-02   0       NaN       NaN\n",
       "2  2021-05-03   0  0.826065  0.793818\n",
       "3  2021-05-04   0       NaN       NaN\n",
       "4  2021-05-05   0  0.824350  0.577807\n",
       "5  2021-05-06   0  0.396992  0.866102\n",
       "6  2021-05-07   0  0.156317  0.289440\n",
       "7  2021-05-01   1  0.737951  0.467681\n",
       "8  2021-05-02   1       NaN       NaN\n",
       "9  2021-05-03   1  0.671271  0.411190\n",
       "10 2021-05-04   1  0.270644  0.427486\n",
       "11 2021-05-05   1       NaN       NaN\n",
       "12 2021-05-06   1  0.992582  0.564232\n",
       "13 2021-05-07   1       NaN       NaN"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "display(date_df_with_missing_dates)\n",
    "tfm = TSAddMissingTimestamps(datetime_col=\"date\", unique_id_cols=\"id\", freq=\"D\", range_by_group=False)\n",
    "df = tfm.fit_transform(date_df_with_missing_dates.copy())\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSDropDuplicates(TransformerMixin, BaseEstimator):\n",
    "    \"Drop rows with duplicated values in a set of columns, optionally including a datetime column or index\"\n",
    "\n",
    "    def __init__(self,\n",
    "        datetime_col=None, #(str or List[str], optional): Name(s) of column(s) containing datetime values. If None, the index is used if use_index=True.\n",
    "        use_index=False, #(bool, optional): Whether to include the index in the set of columns for checking duplicates. Defaults to False.\n",
    "        unique_id_cols=None, #(str or List[str], optional): Name(s) of column(s) to be included in the set of columns for checking duplicates. Defaults to None.\n",
    "        keep='last', #(str, optional): Which duplicated values to keep. Choose from {'first', 'last', False}. Defaults to 'last'.\n",
    "        reset_index=False, #(bool, optional): Whether to reset the index after dropping duplicates. Ignored if use_index=False. Defaults to False.\n",
    "    ):\n",
    "        assert datetime_col is not None or use_index, \"you need to either pass a datetime_col or set use_index=True\"\n",
    "\n",
    "        self.datetime_col, self.use_index, self.unique_id_cols, self.keep, self.reset_index =  \\\n",
    "        listify(datetime_col), use_index, listify(unique_id_cols), keep, reset_index\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.use_index:\n",
    "            cols = [X.index.name or 'index'] + self.unique_id_cols\n",
    "            idxs_to_drop = X.reset_index().duplicated(subset=cols, keep=self.keep)\n",
    "        else:\n",
    "            cols = self.datetime_col + self.unique_id_cols\n",
    "            idxs_to_drop = X.duplicated(subset=cols, keep=self.keep)\n",
    "        if idxs_to_drop.sum():\n",
    "            X.drop(index=idxs_to_drop[idxs_to_drop].index, inplace=True)\n",
    "            if not self.use_index and self.reset_index:\n",
    "                X.reset_index(drop=True, inplace=True)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.201528</td>\n",
       "      <td>0.934433</td>\n",
       "      <td>0.689088</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.016200</td>\n",
       "      <td>0.818380</td>\n",
       "      <td>0.040139</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.016200</td>\n",
       "      <td>0.818380</td>\n",
       "      <td>0.040139</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.889913</td>\n",
       "      <td>0.991963</td>\n",
       "      <td>0.294067</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.865562</td>\n",
       "      <td>0.102843</td>\n",
       "      <td>0.125955</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.979152</td>\n",
       "      <td>0.673839</td>\n",
       "      <td>0.846887</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.979152</td>\n",
       "      <td>0.673839</td>\n",
       "      <td>0.846887</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.603150</td>\n",
       "      <td>0.682532</td>\n",
       "      <td>0.575359</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.429062</td>\n",
       "      <td>0.275923</td>\n",
       "      <td>0.768581</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime  user_id\n",
       "0  0.201528  0.934433  0.689088 2020-01-01        0\n",
       "1  0.016200  0.818380  0.040139 2020-01-03        0\n",
       "2  0.016200  0.818380  0.040139 2020-01-03        0\n",
       "3  0.889913  0.991963  0.294067 2020-01-04        0\n",
       "4  0.865562  0.102843  0.125955 2020-01-06        1\n",
       "5  0.979152  0.673839  0.846887 2020-01-07        1\n",
       "6  0.979152  0.673839  0.846887 2020-01-07        1\n",
       "7  0.603150  0.682532  0.575359 2020-01-09        1\n",
       "8  0.429062  0.275923  0.768581 2020-01-10        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.201528</td>\n",
       "      <td>0.934433</td>\n",
       "      <td>0.689088</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.016200</td>\n",
       "      <td>0.818380</td>\n",
       "      <td>0.040139</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.889913</td>\n",
       "      <td>0.991963</td>\n",
       "      <td>0.294067</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.865562</td>\n",
       "      <td>0.102843</td>\n",
       "      <td>0.125955</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.979152</td>\n",
       "      <td>0.673839</td>\n",
       "      <td>0.846887</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.603150</td>\n",
       "      <td>0.682532</td>\n",
       "      <td>0.575359</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.429062</td>\n",
       "      <td>0.275923</td>\n",
       "      <td>0.768581</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime  user_id\n",
       "0  0.201528  0.934433  0.689088 2020-01-01        0\n",
       "2  0.016200  0.818380  0.040139 2020-01-03        0\n",
       "3  0.889913  0.991963  0.294067 2020-01-04        0\n",
       "4  0.865562  0.102843  0.125955 2020-01-06        1\n",
       "6  0.979152  0.673839  0.846887 2020-01-07        1\n",
       "7  0.603150  0.682532  0.575359 2020-01-09        1\n",
       "8  0.429062  0.275923  0.768581 2020-01-10        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=10)\n",
    "df['user_id'] = np.sort(np.random.randint(0, 2, 10))\n",
    "df = df.iloc[[0, 2, 2, 3, 5, 6, 6, 8, 9]]\n",
    "df.reset_index(drop=True, inplace=True)\n",
    "display(df)\n",
    "tfm = TSDropDuplicates(datetime_col=\"datetime\", unique_id_cols=\"a\")\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSFillMissing(TransformerMixin, BaseEstimator):\n",
    "    \"Fill missing values in specified columns using the specified method and/ or value.\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, #(str or List[str], optional): Column name(s) to be transformed. If None, all columns are transformed. Defaults to None.\n",
    "        unique_id_cols=None, #(str or List[str], optional): Col name(s) with unique ids for each row. If None, uses all rows at once. Defaults to None .\n",
    "        method='ffill', #(str, optional): The method to use for filling missing values, e.g. 'ffill', 'bfill'. If None, `value` is used. Defaults to None.\n",
    "        value=0, #(scalar or dict or Series, optional): The value to use for filling missing values. If None, `method` is used. Defaults to None.\n",
    "    ):\n",
    "\n",
    "        self.columns = listify(columns)\n",
    "        self.unique_id_cols = unique_id_cols\n",
    "        self.method = method\n",
    "        self.value = value\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.method is not None:\n",
    "            for c in self.columns:\n",
    "                if self.unique_id_cols is not None:\n",
    "                    X[c] = X.groupby(self.unique_id_cols)[c].fillna(method=self.method)\n",
    "                else:\n",
    "                    X[c] = X[c].fillna(method=self.method)\n",
    "        if self.value is not None:\n",
    "            for c in self.columns:\n",
    "                X[c] = X[c].fillna(value=self.value)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.059943</td>\n",
       "      <td>0.130974</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.928860</td>\n",
       "      <td>0.331972</td>\n",
       "      <td>0.465337</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.631375</td>\n",
       "      <td>0.426398</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.576881</td>\n",
       "      <td>0.563920</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.500279</td>\n",
       "      <td>0.069394</td>\n",
       "      <td>0.089877</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.600912</td>\n",
       "      <td>0.340959</td>\n",
       "      <td>0.917268</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.143281</td>\n",
       "      <td>0.714719</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.525470</td>\n",
       "      <td>0.697833</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.792191</td>\n",
       "      <td>0.676361</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.945925</td>\n",
       "      <td>0.295824</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.271955</td>\n",
       "      <td>0.217891</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.633712</td>\n",
       "      <td>0.593461</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.016243</td>\n",
       "      <td>0.728778</td>\n",
       "      <td>0.323530</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.556578</td>\n",
       "      <td>0.342731</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>0.134576</td>\n",
       "      <td>0.094419</td>\n",
       "      <td>0.831518</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id\n",
       "0        NaN  0.059943  0.130974 2020-01-01        0\n",
       "1   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "2   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "3   0.928860  0.331972  0.465337 2020-01-04        0\n",
       "4        NaN  0.631375  0.426398 2020-01-06        0\n",
       "5   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "6   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "7        NaN  0.576881  0.563920 2020-01-09        0\n",
       "8   0.500279  0.069394  0.089877 2020-01-10        0\n",
       "9   0.600912  0.340959  0.917268 2020-01-11        0\n",
       "10  0.406591  0.143281  0.714719 2020-01-12        0\n",
       "11       NaN  0.525470  0.697833 2020-01-13        1\n",
       "12       NaN  0.792191  0.676361 2020-01-14        1\n",
       "13       NaN  0.945925  0.295824 2020-01-15        1\n",
       "14       NaN  0.271955  0.217891 2020-01-16        1\n",
       "15       NaN  0.633712  0.593461 2020-01-17        1\n",
       "16  0.016243  0.728778  0.323530 2020-01-18        1\n",
       "17       NaN  0.556578  0.342731 2020-01-19        1\n",
       "18  0.134576  0.094419  0.831518 2020-01-20        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.059943</td>\n",
       "      <td>0.130974</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.928860</td>\n",
       "      <td>0.331972</td>\n",
       "      <td>0.465337</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.928860</td>\n",
       "      <td>0.631375</td>\n",
       "      <td>0.426398</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.576881</td>\n",
       "      <td>0.563920</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.500279</td>\n",
       "      <td>0.069394</td>\n",
       "      <td>0.089877</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.600912</td>\n",
       "      <td>0.340959</td>\n",
       "      <td>0.917268</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.143281</td>\n",
       "      <td>0.714719</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.525470</td>\n",
       "      <td>0.697833</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.792191</td>\n",
       "      <td>0.676361</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.945925</td>\n",
       "      <td>0.295824</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.271955</td>\n",
       "      <td>0.217891</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.633712</td>\n",
       "      <td>0.593461</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.016243</td>\n",
       "      <td>0.728778</td>\n",
       "      <td>0.323530</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>0.016243</td>\n",
       "      <td>0.556578</td>\n",
       "      <td>0.342731</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>0.134576</td>\n",
       "      <td>0.094419</td>\n",
       "      <td>0.831518</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id\n",
       "0   0.000000  0.059943  0.130974 2020-01-01        0\n",
       "1   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "2   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "3   0.928860  0.331972  0.465337 2020-01-04        0\n",
       "4   0.928860  0.631375  0.426398 2020-01-06        0\n",
       "5   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "6   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "7   0.548145  0.576881  0.563920 2020-01-09        0\n",
       "8   0.500279  0.069394  0.089877 2020-01-10        0\n",
       "9   0.600912  0.340959  0.917268 2020-01-11        0\n",
       "10  0.406591  0.143281  0.714719 2020-01-12        0\n",
       "11  0.406591  0.525470  0.697833 2020-01-13        1\n",
       "12  0.406591  0.792191  0.676361 2020-01-14        1\n",
       "13  0.406591  0.945925  0.295824 2020-01-15        1\n",
       "14  0.406591  0.271955  0.217891 2020-01-16        1\n",
       "15  0.406591  0.633712  0.593461 2020-01-17        1\n",
       "16  0.016243  0.728778  0.323530 2020-01-18        1\n",
       "17  0.016243  0.556578  0.342731 2020-01-19        1\n",
       "18  0.134576  0.094419  0.831518 2020-01-20        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(20,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df.loc[np.random.rand(20) > .5, 'a'] = np.nan\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=20)\n",
    "df['user_id'] = np.sort(np.random.randint(0, 2, 20))\n",
    "df = df.iloc[[0, 2, 2, 3, 5, 6, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]\n",
    "df.reset_index(drop=True, inplace=True)\n",
    "display(df)\n",
    "tfm = TSFillMissing(columns=\"a\", method=\"ffill\", value=0)\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)\n",
    "test_eq(df['a'].isna().sum(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSMissingnessEncoder(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, columns=None):\n",
    "        self.columns = listify(columns)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        self.missing_columns = [f\"{cn}_missing\" for cn in self.columns]\n",
    "        return self\n",
    "\n",
    "    def transform(self, X:pd.DataFrame, y=None, **transform_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        X[self.missing_columns] = X[self.columns].isnull().astype(np.int16)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.873619</td>\n",
       "      <td>0.995569</td>\n",
       "      <td>0.582714</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.133210</td>\n",
       "      <td>0.632396</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.700611</td>\n",
       "      <td>0.753472</td>\n",
       "      <td>0.872859</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.617106</td>\n",
       "      <td>0.849959</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.196246</td>\n",
       "      <td>0.125550</td>\n",
       "      <td>0.963480</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.108045</td>\n",
       "      <td>0.478491</td>\n",
       "      <td>0.585564</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.086032</td>\n",
       "      <td>0.057027</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>0.105483</td>\n",
       "      <td>0.585588</td>\n",
       "      <td>0.544345</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>0.233741</td>\n",
       "      <td>0.637774</td>\n",
       "      <td>0.820068</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.498130</td>\n",
       "      <td>0.689310</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.307771</td>\n",
       "      <td>0.613638</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>0.897935</td>\n",
       "      <td>0.809924</td>\n",
       "      <td>0.583130</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.730222</td>\n",
       "      <td>0.364822</td>\n",
       "      <td>0.640966</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>0.466182</td>\n",
       "      <td>0.189936</td>\n",
       "      <td>0.701738</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.358622</td>\n",
       "      <td>0.911339</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id\n",
       "0   0.873619  0.995569  0.582714 2020-01-01        0\n",
       "1   0.402704  0.672507  0.682192 2020-01-03        0\n",
       "2   0.402704  0.672507  0.682192 2020-01-03        0\n",
       "3        NaN  0.133210  0.632396 2020-01-04        0\n",
       "4   0.700611  0.753472  0.872859 2020-01-06        0\n",
       "5        NaN  0.730249  0.619173 2020-01-07        0\n",
       "6        NaN  0.730249  0.619173 2020-01-07        0\n",
       "7        NaN  0.617106  0.849959 2020-01-09        0\n",
       "8   0.196246  0.125550  0.963480 2020-01-10        1\n",
       "9   0.108045  0.478491  0.585564 2020-01-11        1\n",
       "10       NaN  0.086032  0.057027 2020-01-12        1\n",
       "11  0.105483  0.585588  0.544345 2020-01-13        1\n",
       "12  0.233741  0.637774  0.820068 2020-01-14        1\n",
       "13       NaN  0.498130  0.689310 2020-01-15        1\n",
       "14       NaN  0.307771  0.613638 2020-01-16        1\n",
       "15  0.897935  0.809924  0.583130 2020-01-17        1\n",
       "16  0.730222  0.364822  0.640966 2020-01-18        1\n",
       "17  0.466182  0.189936  0.701738 2020-01-19        1\n",
       "18       NaN  0.358622  0.911339 2020-01-20        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "      <th>a_missing</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.873619</td>\n",
       "      <td>0.995569</td>\n",
       "      <td>0.582714</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.133210</td>\n",
       "      <td>0.632396</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.700611</td>\n",
       "      <td>0.753472</td>\n",
       "      <td>0.872859</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.617106</td>\n",
       "      <td>0.849959</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.196246</td>\n",
       "      <td>0.125550</td>\n",
       "      <td>0.963480</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.108045</td>\n",
       "      <td>0.478491</td>\n",
       "      <td>0.585564</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.086032</td>\n",
       "      <td>0.057027</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>0.105483</td>\n",
       "      <td>0.585588</td>\n",
       "      <td>0.544345</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>0.233741</td>\n",
       "      <td>0.637774</td>\n",
       "      <td>0.820068</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.498130</td>\n",
       "      <td>0.689310</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.307771</td>\n",
       "      <td>0.613638</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>0.897935</td>\n",
       "      <td>0.809924</td>\n",
       "      <td>0.583130</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.730222</td>\n",
       "      <td>0.364822</td>\n",
       "      <td>0.640966</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>0.466182</td>\n",
       "      <td>0.189936</td>\n",
       "      <td>0.701738</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.358622</td>\n",
       "      <td>0.911339</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id  a_missing\n",
       "0   0.873619  0.995569  0.582714 2020-01-01        0          0\n",
       "1   0.402704  0.672507  0.682192 2020-01-03        0          0\n",
       "2   0.402704  0.672507  0.682192 2020-01-03        0          0\n",
       "3        NaN  0.133210  0.632396 2020-01-04        0          1\n",
       "4   0.700611  0.753472  0.872859 2020-01-06        0          0\n",
       "5        NaN  0.730249  0.619173 2020-01-07        0          1\n",
       "6        NaN  0.730249  0.619173 2020-01-07        0          1\n",
       "7        NaN  0.617106  0.849959 2020-01-09        0          1\n",
       "8   0.196246  0.125550  0.963480 2020-01-10        1          0\n",
       "9   0.108045  0.478491  0.585564 2020-01-11        1          0\n",
       "10       NaN  0.086032  0.057027 2020-01-12        1          1\n",
       "11  0.105483  0.585588  0.544345 2020-01-13        1          0\n",
       "12  0.233741  0.637774  0.820068 2020-01-14        1          0\n",
       "13       NaN  0.498130  0.689310 2020-01-15        1          1\n",
       "14       NaN  0.307771  0.613638 2020-01-16        1          1\n",
       "15  0.897935  0.809924  0.583130 2020-01-17        1          0\n",
       "16  0.730222  0.364822  0.640966 2020-01-18        1          0\n",
       "17  0.466182  0.189936  0.701738 2020-01-19        1          0\n",
       "18       NaN  0.358622  0.911339 2020-01-20        1          1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(20,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df.loc[np.random.rand(20) > .5, 'a'] = np.nan\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=20)\n",
    "df['user_id'] = np.sort(np.random.randint(0, 2, 20))\n",
    "df = df.iloc[[0, 2, 2, 3, 5, 6, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]\n",
    "df.reset_index(drop=True, inplace=True)\n",
    "display(df)\n",
    "tfm = TSMissingnessEncoder(columns=\"a\")\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With these sklearn preprocessing API transforms it's possible to build data preprocessing pipelines like this one:\n",
    "\n",
    "```python\n",
    "from sklearn.pipeline import Pipeline\n",
    "\n",
    "cont_cols = ['cont_0', 'cont_1', 'cont_2', 'cont_3', 'cont_4', 'cont_5']\n",
    "pipe = Pipeline([\n",
    "    ('shrinker', TSShrinkDataFrame()), \n",
    "    ('drop_duplicates', TSDropDuplicates('date', unique_id_cols='user_id')),\n",
    "    ('add_mts', TSAddMissingTimestamps(datetime_col='date', unique_id_cols='user_id', freq='D', range_by_group=False)),\n",
    "    ('onehot_encoder', TSOneHotEncoder(['cat_0'])),\n",
    "    ('cat_encoder', TSCategoricalEncoder(['user_id', 'cat_1'])),\n",
    "    ('steps_since_start', TSStepsSinceStart('date', datetime_unit='D', start_datetime='2017-01-01'), dtype=np.int32),\n",
    "    ('missing_encoder', TSMissingnessEncoder(['cont_1'])),\n",
    "    ('fill_missing', TSFillMissing(cont_cols, unique_id_cols='user_id', value=0)),\n",
    "    ], \n",
    "    verbose=True)\n",
    "df = pipe.fit_transform(df)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## y transforms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class Preprocessor():\n",
    "    def __init__(self, preprocessor, **kwargs):\n",
    "        self.preprocessor = preprocessor(**kwargs)\n",
    "\n",
    "    def fit(self, o):\n",
    "        if isinstance(o, pd.Series): o = o.values.reshape(-1,1)\n",
    "        else: o = o.reshape(-1,1)\n",
    "        self.fit_preprocessor = self.preprocessor.fit(o)\n",
    "        return self.fit_preprocessor\n",
    "\n",
    "    def transform(self, o, copy=True):\n",
    "        if type(o) in [float, int]: o = array([o]).reshape(-1,1)\n",
    "        o_shape = o.shape\n",
    "        if isinstance(o, pd.Series): o = o.values.reshape(-1,1)\n",
    "        else: o = o.reshape(-1,1)\n",
    "        output = self.fit_preprocessor.transform(o).reshape(*o_shape)\n",
    "        if isinstance(o, torch.Tensor): return o.new(output)\n",
    "        return output\n",
    "\n",
    "    def inverse_transform(self, o, copy=True):\n",
    "        o_shape = o.shape\n",
    "        if isinstance(o, pd.Series): o = o.values.reshape(-1,1)\n",
    "        else: o = o.reshape(-1,1)\n",
    "        output = self.fit_preprocessor.inverse_transform(o).reshape(*o_shape)\n",
    "        if isinstance(o, torch.Tensor): return o.new(output)\n",
    "        return output\n",
    "\n",
    "\n",
    "StandardScaler = partial(sklearn.preprocessing.StandardScaler)\n",
    "setattr(StandardScaler, '__name__', 'StandardScaler')\n",
    "RobustScaler = partial(sklearn.preprocessing.RobustScaler)\n",
    "setattr(RobustScaler, '__name__', 'RobustScaler')\n",
    "Normalizer = partial(sklearn.preprocessing.MinMaxScaler, feature_range=(-1, 1))\n",
    "setattr(Normalizer, '__name__', 'Normalizer')\n",
    "BoxCox = partial(sklearn.preprocessing.PowerTransformer, method='box-cox')\n",
    "setattr(BoxCox, '__name__', 'BoxCox')\n",
    "YeoJohnshon = partial(sklearn.preprocessing.PowerTransformer, method='yeo-johnson')\n",
    "setattr(YeoJohnshon, '__name__', 'YeoJohnshon')\n",
    "Quantile = partial(sklearn.preprocessing.QuantileTransformer, n_quantiles=1_000, output_distribution='normal', random_state=0)\n",
    "setattr(Quantile, '__name__', 'Quantile')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Standardize\n",
    "from tsai.data.validation import TimeSplitter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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I22oRkbilJdRNgBAxRNIAIDsBBIAIDuBBADITiABALITSACA7AQSACA7gQQAyE4gAQCyE0gAgOwEEgAgu2NyF6A1s74MB9MS1q8BaEm0kAAA2QkkAEB2AgkAkJ1AAgBkJ5AAANlV3Sibco18WTN1ZFkeh+obWdN357yopDJXUnmBeF/fcy3xO0wLCQCQnUACAGQnkAAA2QkkAEB2AgkAkF3VjbKpxHVqrInTetexaSlr2uw7AsjIGqrF0Rx1WakjX44mLSQAQOsNJDNmzIi+fftGTU1NXHDBBfHMM88cqacCACrcEQkkP/zhD2PixIlx5513xvPPPx9nn312DB8+PDZv3nwkng4AqHBHJJDcd999cdNNN8WNN94YAwcOjIceeiiOPfbY+Md//Mcj8XQAQIUre6fWXbt2xcqVK2Py5MmN+9q2bRvDhg2L5cuXv+v8+vr6YmuwdevW4ue2bdviSNhb//YReVx4L9valKKlS++PVM6G98m+t4EDez/fV+/nfbStTN97R/O5DvSYpdJhftaVyuyNN95IJSk99dRTTfbfdtttpfPPP/9d5995553F+TabzWaz2aLit3Xr1h1Wfsg+7De1pKT+Jg327t0b//3f/x3du3ePNm3aRGuXEuUpp5wS69atiy5duuQuToujfg5N/Rya+jk09XNo6qd59ZNaRrZv3x69evWKw1H2QHLiiSdGu3btYtOmTU32p/t1dXXvOr9jx47Ftq+uXbtGtUn/M73gD079HJr6OTT1c2jq59DUz/uvn9ra2mgxnVo7dOgQgwcPjiVLljRp9Uj3hwwZUu6nAwBagSNyySZdgrn++uvjYx/7WJx//vlx//33x44dO4pRNwAARyWQXHPNNfHmm2/GV7/61di4cWOcc845sXjx4ujRo8eReLqKli5Xpfla9r9sxf9SP4emfg5N/Rya+jk09XN066dN6tlalkcCADhM1rIBALITSACA7AQSACA7gQQAyE4gyWDNmjUxevTo6NevX3Tq1Ck+9KEPFT2V0zpA+/rVr34Vn/jEJ6KmpqaYDe/ee++NavH1r389Pv7xjxeLMh5sory1a9fGyJEji3NOPvnkuO222+Kdd96JajFjxozo27dv8fq44IIL4plnnolq9eSTT8YVV1xRzBCZZnh+5JFHmhxPfffTqL+ePXsW77m0ttbLL78c1WDKlClx3nnn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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "y = random_shuffle(np.random.randn(1000) * 10 + 5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(StandardScaler)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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hA8AICsdTgE4bTq30hhaPgCArIQPACAr4QMAyEr4AACyEj4AgKyEDwAgK+EDAMhK+AAAshI+AICshA8AICvhAwDIytouNJo1HJrX1srrY/Ch5S1dDIBmo+UDAMhK+AAAshI+AICshA8AICvhAwDIymgXKANG0NDcI9ygMbR8AABZCR8AQFbCBwCQlfABAGQlfAAAWQkfAEBWwgcAkJXwAQBkJXwAAFkJHwBAVsIHAJCVtV1oFtaCOP21WADaq0a3fDz++ONxzTXXRP/+/aNDhw7x8MMP1zteKpXiq1/9avTr1y+6desWY8eOjeeff74pywwAlFP4OHjwYFxyySWxcOHCkx6/66674v77748HH3wwNm7cGGeeeWaMGzcuDh061BTlBQDK7bLL+PHji+1kUqvHvffeG1/+8pdj4sSJxb5vf/vb0bdv36KF5BOf+MTplxgAaNOatMPpSy+9FLt27SoutdTq0aNHjBo1KmpqapryqQCANqpJO5ym4JGklo4Tpfu1x97o8OHDxVZr//79TVkkAKCVafGhtvPmzStaR2q3gQMHtnSRAIC2Ej6qq6uLn7t37663P92vPfZGc+bMiX379tVt27dvb8oiAQDtOXwMGTKkCBlr166tdxkljXoZPXr0Sf9P165do6qqqt4GALRfje7z8dprr8ULL7xQr5Pp5s2bo1evXjFo0KCYMWNG3HnnnfGud72rCCNf+cpXijlBrr322qYuOwBQDuHjqaeeig9/+MN192fNmlX8nDJlSixbtiy++MUvFnOB3HzzzbF379646qqrYs2aNVFZWdm0JQcA2qQOpTQ5RyuSLtOkjqep/4dLMK2TqdPb5tTqgw8tb5bHhYbaOn9CSxeBVvL53eKjXQCA8iJ8AABZCR8AQFbCBwCQlfABALTdtV1o3aNL9DRv/aNcjEgByoGWDwAgK+EDAMhK+AAAshI+AICshA8AICujXajHui0ANDctHwBAVsIHAJCV8AEAZCV8AABZCR8AQFZGu0CZsHYMbWE0XUPWoGqqx6HlaPkAALISPgCArIQPACAr4QMAyEr4AACyMtqljFi3pW2MSAFo77R8AABZCR8AQFbCBwCQlfABAGQlfAAAWRnt0oysP0BrXXOldlSNtV4oZ001AtD7eONp+QAAshI+AICshA8AICvhAwDISvgAALIy2qWFWW+FnGu6vNXI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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# RobustScaler\n",
    "y = random_shuffle(np.random.randn(1000) * 10 + 5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(RobustScaler)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Normalize\n",
    "y = random_shuffle(np.random.rand(1000) * 3 + .5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(Normalizer)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGdCAYAAACyzRGfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAI5FJREFUeJzt3Q1UVGUex/E/qKCmwuIbEKiopeZbrRm5lutbEnlMy0p71c2j6bF2lTSlo6XWHlzrlNUxandNejOr3dTTmx61wFOhKcZqVp7gYOKKL9lhQFjRhbvnebYZGQV0YOaZuTPfzzlXmDuXmWfu3Lnz87nP/d8wy7IsAQAAMCTc1BMBAAAohA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARjWXAFNTUyNHjhyRtm3bSlhYmL+bAwAALoGqWVpeXi7x8fESHh5ur/ChgkdiYqK/mwEAABqhuLhYEhIS7BU+VI+Hs/Ht2rXzd3MAAMAlKCsr050Hzu9xW4UP56EWFTwIHwAA2MulDJlgwCkAADCK8AEAAIwifAAAAKMCbswHAAD+PF30v//9r1RXV/u7KQGpRYsW0qxZsyY/DuEDAAAROXPmjJSUlEhlZaW/mxLQg0nVabRt2rRp0uMQPgAAIU8VuCwqKtL/q1dFsiIiIih0WUev0IkTJ+Tw4cNyxRVXNKkHhPABAAh5qtdDBRBVp6J169b+bk7A6tixoxw8eFDOnj3bpPDBgFMAAH51sbLgoS7MS71BrGUAAGAU4QMAABjFmA8AABrQbeHHRp/v4PKxRp5nyZIlsmHDBsnPzxfT6PkAACAEzZs3T7Zt2+aX56bnAwCAEDtltrq6WtfqaGq9jsai5wMAAJurqqqSP/7xj9KpUydp2bKl3HDDDbJr1y59X3Z2tj5L5dNPP5VBgwZJZGSkfPHFF/qwy9VXX+2X9hI+0HRLov4/AQD84rHHHpN//vOf8vrrr8uePXukZ8+ekpKSIr/88otrmYULF8ry5cvl+++/lwEDBvi1vRx2AQDAxioqKiQzM1OysrIkNTVVz/vb3/4mW7ZskdWrV8vgwYP1vGXLlslNN90kgYCeDwAAbKywsFBXHB06dKjbBeCuu+463cvhdO2110qgIHwAABACLrvsMgkUhA8AAGysR48e+kJ4X375pWue6glRA06vuuoqCUSM+QAAwOY9GrNmzZL58+dLTEyMdOnSRVasWCGVlZUybdo0+de//iWBhvABAEAAVBxtCnUWi7oq7/333y/l5eV6fMfmzZvlN7/5jQSiMEtVGwkgZWVlEhUVJQ6HQ9q1a+fv5uBSOE+zXeLwd0sAoFFOnz4tRUVFkpSUpOtkwPP15Mn3N2M+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAAARu+FBXzVOX4VXn76ppyJAh8umnn7ruHz58uISFhblNM2fO9EW7AQBAKISPhIQEXUUtLy9Pdu/eLSNHjpTx48fL/v37XctMnz5dSkpKXJMq8QoAAMz58ssvpX///vrqthMmTBBbl1cfN26c2+0///nPujdkx44d0rdvXz2vdevWEhsb691WAgDg7yrOxp7P4dHi6qjD1VdfLStXrnTNS0tL0/PU0Yk2bdpI0Iz5qK6ulnXr1klFRYU+/OL09ttvS4cOHaRfv36Snp6uL2zTkKqqKl2StfYEAAAar7CwUB+dUEcsoqOjxfbhY9++fTpFRUZG6vEc69evd12y95577pG33npLPv/8cx083nzzTbnvvvsafLyMjAxdC945JSYmNv7VAAAQQqZOnSo5OTnywgsvuI23PHnypDz44IP696ysLMnOzta/q4vNXXPNNdKqVSsdTo4fP657R/r06aPHcqrv8Yt1GvjlwnJnzpyRQ4cO6QvH/OMf/5C///3v+oU7A0htn332mYwaNUoKCgqkR48e9fZ8qMlJ9XyoAMKF5WyEC8sBCOYLywXwYReHwyGpqan6aMOyZcv0UQlFfSer25MmTdL/sd+5c6eMGDFCrr/+enn22Wf1EIm77rpLLr/8ct2ZoMZznjp1Sm677TaZP3++LFiwwKcXlvNozIcSEREhPXv21L8PGjRIdu3apRPXq6++esGyycnJ+mdD4UO9aDUBAADPqC979b18/nhL1cuh7jt/DObTTz8tQ4cO1b9PmzZNH6VQh2i6d++u591xxx366EV94SNg6nzU1NS49VzUlp+fr3/GxcU19WkAAEATqXIZTp07d9ahxRk8nPPUoRhf86jnQyUk1b3TpUsXKS8vl7Vr1+rjSOoYkkpO6vYtt9wi7du3l71798rcuXNl2LBhbi8WAAD4hzr1tnbvSO3bznmqUyGgwodKQw888ICu36G6c1SoUMHjpptukuLiYtm6das+1UedAaPGbUycOFEWLVrku9YDABDiIiIiXGM97MKj8LF69ep671NhQw08BQAA5nTr1k0PKD148KA+GzUmJkYCHdd2AQDAxubNmyfNmjXTZ7h07NhRn5Ea6Dw+1dbXPDlVBwGCU20BBPOptvD6qbb0fAAAAKMIHwAAwCjCBwAAMIrwAQAAjCJ8AAAAowgfAAD8KsBOAA3a9UP4AACEPGeZcROXk7czdWV7RdUVaQqPr2oLAECwUV+m0dHRrouqqQuuqeuc4Bx1zZcTJ07oddO8edPiA+EDAAAR1+XnTVzV1a7Cw8P1xWWbGswIHwAA/HpF17i4OOnUqZOcPXvW380J2IvYqQDSVIQPAADOOwTT1DENaBgDTgEAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAIEbPjIzM2XAgAHSrl07PQ0ZMkQ+/fRT1/2nT5+W2bNnS/v27aVNmzYyceJEOXbsmC/aDQAAQiF8JCQkyPLlyyUvL092794tI0eOlPHjx8v+/fv1/XPnzpUPP/xQ3n//fcnJyZEjR47I7bff7qu2AwAAGwqzLMtqygPExMTIM888I3fccYd07NhR1q5dq39XfvjhB+nTp4/k5ubK9ddff0mPV1ZWJlFRUeJwOHTvCmxgSdSvPx3+bgkAwE88+f5u9JiP6upqWbdunVRUVOjDL6o35OzZszJ69GjXMr1795YuXbro8FGfqqoq3eDaEwAACF4eh499+/bp8RyRkZEyc+ZMWb9+vVx11VVy9OhRiYiIkOjoaLflO3furO+rT0ZGhk5KzikxMbFxrwQAAARn+OjVq5fk5+fLzp07ZdasWTJlyhT57rvvGt2A9PR03UXjnIqLixv9WAAAIPA19/QPVO9Gz5499e+DBg2SXbt2yQsvvCCTJk2SM2fOSGlpqVvvhzrbJTY2tt7HUz0oagIAAKGhyXU+ampq9LgNFURatGgh27Ztc9134MABOXTokB4TAgAA4HHPhzpEkpqaqgeRlpeX6zNbsrOzZfPmzXq8xrRp0yQtLU2fAaNGuj7yyCM6eFzqmS4AACD4eRQ+jh8/Lg888ICUlJTosKEKjqngcdNNN+n7n3/+eQkPD9fFxVRvSEpKirz88su+ajsAAAjFOh/eRp0PG6LOBwCEvDITdT4AAAAag/ABAAAC+1RbBNnhDQ6ZALChbgs/vmDeweVj/dIWeI6eDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgIZEui/j819v7aywEAECAIHwAAwCjCBwAAMIrwAQAAjCJ8AAAAowgfAADAKMIHAAAwivABAACMInwAAACjCB8AAMCo5mafDl5BxVIgpHVb+HGd8w8uH3tJy9a1HPyvWwi9V/R8AAAAowgfAADAKMIHAAAwivABAAACN3xkZGTI4MGDpW3bttKpUyeZMGGCHDhwwG2Z4cOHS1hYmNs0c+ZMb7cbAACEQvjIycmR2bNny44dO2TLli1y9uxZGTNmjFRUVLgtN336dCkpKXFNK1as8Ha7AQBAKJxqu2nTJrfbWVlZugckLy9Phg0b5prfunVriY2N9V4rAQBA0GjSmA+Hw6F/xsTEuM1/++23pUOHDtKvXz9JT0+XysrKeh+jqqpKysrK3CYAABC8Gl1krKamRubMmSNDhw7VIcPpnnvuka5du0p8fLzs3btXFixYoMeFfPDBB/WOI1m6dKkEZBGvJf8PVzgPRc4A2KRIlicF2QKhXaGi0eFDjf349ttv5YsvvnCbP2PGDNfv/fv3l7i4OBk1apQUFhZKjx49Lngc1TOSlpbmuq16PhITExvbLAAAEIzh4+GHH5aPPvpItm/fLgkJCQ0um5ycrH8WFBTUGT4iIyP1BAAAQoNH4cOyLHnkkUdk/fr1kp2dLUlJSRf9m/z8fP1T9YAAAAA09/RQy9q1a2Xjxo261sfRo0f1/KioKGnVqpU+tKLuv+WWW6R9+/Z6zMfcuXP1mTADBgzw1WsAAADBGj4yMzNdhcRqW7NmjUydOlUiIiJk69atsnLlSl37Q43dmDhxoixatMi7rQYAAKFz2KUhKmyoQmQAAAD14douAADAKMIHAAAwivABAADsUWQMQVoZ1ZuvIRjWBwKSJ9UhTVayDMQKn0AgoucDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBRFxhqD4lmwUeGtpha5onCWfQqq+eK5eK/t8/7ZCT0fAADAKMIHAAAwivABAACMInwAAACjCB8AAMAowgcAADCK8AEAAIwifAAAAKMIHwAAwCgqnAaTQKm86mwHAoKpqpW+qrDqC3ZqKwIT1WCbhp4PAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFEUGfNVkS1/FvpqTLGx+gqDBUrhMgQVCjQFZpG1UNfU7ZL1euno+QAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAAgRs+MjIyZPDgwdK2bVvp1KmTTJgwQQ4cOOC2zOnTp2X27NnSvn17adOmjUycOFGOHTvm7XYDAIBQCB85OTk6WOzYsUO2bNkiZ8+elTFjxkhFRYVrmblz58qHH34o77//vl7+yJEjcvvtt/ui7QAAINjrfGzatMntdlZWlu4BycvLk2HDhonD4ZDVq1fL2rVrZeTIkXqZNWvWSJ8+fXRguf76673begAAEFpjPlTYUGJiYvRPFUJUb8jo0aNdy/Tu3Vu6dOkiubm5dT5GVVWVlJWVuU0AACB4NbrCaU1NjcyZM0eGDh0q/fr10/OOHj0qEREREh0d7bZs586d9X31jSNZunSp2KL6py+qfJp4bG/+bVMeE0EjlCo52qkaq6n3JZTef3/r1sR1HajbaqN7PtTYj2+//VbWrVvXpAakp6frHhTnVFxc3KTHAwAAQdjz8fDDD8tHH30k27dvl4SEBNf82NhYOXPmjJSWlrr1fqizXdR9dYmMjNQTAAAIDR71fFiWpYPH+vXr5bPPPpOkpCS3+wcNGiQtWrSQbdu2ueapU3EPHTokQ4YM8V6rAQBAaPR8qEMt6kyWjRs36lofznEcUVFR0qpVK/1z2rRpkpaWpgehtmvXTh555BEdPDjTBQAAeBw+MjMz9c/hw4e7zVen006dOlX//vzzz0t4eLguLqbOZElJSZGXX36ZtQ0AADwPH+qwy8W0bNlSVq1apScAAIDzcW0XAABgFOEDAAAYRfgAAAD2qHAKL1c09WWlU8DGQqmaZjC+VjtViIU59HwAAACjCB8AAMAowgcAADCK8AEAAIwifAAAAKMIHwAAwCjCBwAAMIrwAQAAjArdImO+LuoVaEXDTLTH+RwIeBR+AuBP9HwAAACjCB8AAMAowgcAADCK8AEAAIwifAAAAKMIHwAAwCjCBwAAMIrwAQAAjCJ8AAAAo0K3wqnpSqRNqf4ZCJVDvdH+QKn2CgRghdlQUt/rN1llN9TfA3+j5wMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFEXGmuJSimfVV5wrEAqHAQEkEIo+XWobPGlrILyuprB7+xGY6PkAAABGET4AAIBRhA8AAGAU4QMAAAR2+Ni+fbuMGzdO4uPjJSwsTDZs2OB2/9SpU/X82tPNN9/szTYDAIBQCh8VFRUycOBAWbVqVb3LqLBRUlLimt55552mthMAAITqqbapqal6akhkZKTExsY2pV0AACBI+WTMR3Z2tnTq1El69eols2bNkpMnT/riaQAAgA15vciYOuRy++23S1JSkhQWFsrjjz+ue0pyc3OlWbNmFyxfVVWlJ6eysjJvNwkAAARz+Jg8ebLr9/79+8uAAQOkR48eujdk1KhRFyyfkZEhS5cuFVvzZrXSxjyWqb8BbIgKnfbBexU6fH6qbffu3aVDhw5SUFBQ5/3p6enicDhcU3Fxsa+bBAAAgvnaLocPH9ZjPuLi4uodnKomAAAQGjwOH6dOnXLrxSgqKpL8/HyJiYnRkzqEMnHiRH22ixrz8dhjj0nPnj0lJSXF220HAAChED52794tI0aMcN1OS0vTP6dMmSKZmZmyd+9eef3116W0tFQXIhszZow89dRT9G4AAIDGhY/hw4eLZVn13r9582ZPHxIAAIQQru0CAACMInwAAACjCB8AACC4TrW1DVV0a4njwnkAmoTCUbDztmb37bdbPe0/uHys+BM9HwAAwCjCBwAAMIrwAQAAjCJ8AAAAowgfAADAKMIHAAAwivABAACMInwAAACjCB8AAMAoKpzWRkVTAAB8jp4PAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFEUGQOgdVv4sb+bACBE0PMBAACMInwAAACjCB8AAMAowgcAADCK8AEAAIwifAAAAKMIHwAAwCjCBwAAMIrwAQAAjKLCaahZEuXvFgAAQhw9HwAAwCjCBwAAMIrwAQAAjCJ8AACAwA4f27dvl3Hjxkl8fLyEhYXJhg0b3O63LEueeOIJiYuLk1atWsno0aPlxx9/9GabAQBAKIWPiooKGThwoKxatarO+1esWCEvvviivPLKK7Jz50657LLLJCUlRU6fPu2N9gIAgFA71TY1NVVPdVG9HitXrpRFixbJ+PHj9bw33nhDOnfurHtIJk+e3PQWAwAAW/PqmI+ioiI5evSoPtTiFBUVJcnJyZKbm1vn31RVVUlZWZnbBAAAgpdXw4cKHorq6ahN3Xbed76MjAwdUJxTYmKiBHSBLop0AQBg77Nd0tPTxeFwuKbi4mJ/NwkAANglfMTGxuqfx44dc5uvbjvvO19kZKS0a9fObQIAAMHLq+EjKSlJh4xt27a55qkxHOqslyFDhnjzqQAAQKic7XLq1CkpKChwG2San58vMTEx0qVLF5kzZ448/fTTcsUVV+gwsnjxYl0TZMKECd5uOwAACIXwsXv3bhkxYoTrdlpamv45ZcoUycrKkscee0zXApkxY4aUlpbKDTfcIJs2bZKWLVt6t+UAACA0wsfw4cN1PY/6qKqny5Yt0xMAAEDAne0CAABCC+EDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABg7/CxZMkSCQsLc5t69+7t7acBAAA21dwXD9q3b1/ZunXruSdp7pOnAQAANuSTVKDCRmxsrC8eGgAA2JxPxnz8+OOPEh8fL927d5d7771XDh06VO+yVVVVUlZW5jYBAIDg5fXwkZycLFlZWbJp0ybJzMyUoqIiufHGG6W8vLzO5TMyMiQqKso1JSYmertJAAAgmMNHamqq3HnnnTJgwABJSUmRTz75REpLS+W9996rc/n09HRxOByuqbi42NtNAgAAAcTnI0Gjo6PlyiuvlIKCgjrvj4yM1BMAAAgNPq/zcerUKSksLJS4uDhfPxUAAAjF8DFv3jzJycmRgwcPyldffSW33XabNGvWTO6++25vPxUAALAhrx92OXz4sA4aJ0+elI4dO8oNN9wgO3bs0L8DAAB4PXysW7fO2w8JAACCCNd2AQAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAABEf4WLVqlXTr1k1atmwpycnJ8vXXX/vqqQAAQKiHj3fffVfS0tLkySeflD179sjAgQMlJSVFjh8/7ounAwAAoR4+nnvuOZk+fbr84Q9/kKuuukpeeeUVad26tbz22mu+eDoAAGAjzb39gGfOnJG8vDxJT093zQsPD5fRo0dLbm7uBctXVVXpycnhcOifZWVl3m7ar09o+eZxcXG+ek+h1VRV+rsJAGyizAf7Y+djWpZlPnz8/PPPUl1dLZ07d3abr27/8MMPFyyfkZEhS5cuvWB+YmKit5sGf1se5e8WAABEJGql7x67vLxcoqKizIYPT6keEjU+xKmmpkZ++eUXad++vYSFhbnSlAojxcXF0q5dOwllrItzWBfnsC7OYV2cw7o4h3Xh+3WhejxU8IiPj7/osl4PHx06dJBmzZrJsWPH3Oar27GxsRcsHxkZqafaoqOj63xstZJCfaNxYl2cw7o4h3VxDuviHNbFOawL366Li/V4+GzAaUREhAwaNEi2bdvm1puhbg8ZMsTbTwcAAGzGJ4dd1GGUKVOmyLXXXivXXXedrFy5UioqKvTZLwAAILT5JHxMmjRJTpw4IU888YQcPXpUrr76atm0adMFg1AvlToso2qGnH94JhSxLs5hXZzDujiHdXEO6+Ic1kVgrYsw61LOiQEAAPASru0CAACMInwAAACjCB8AAMAowgcAADAqIMPHwYMHZdq0aZKUlCStWrWSHj166JG56roxDRk+fLiuilp7mjlzptjNqlWrpFu3btKyZUtJTk6Wr7/+usHl33//fendu7devn///vLJJ5+I3amy+4MHD5a2bdtKp06dZMKECXLgwIEG/yYrK+uC91+tE7tbsmTJBa9Lvd+htk0o6nNx/rpQ0+zZs4N+m9i+fbuMGzdOV49Ur2PDhg1u96tzB9QZhnFxcXq/qa6n9eOPP3p9fxPo6+Ls2bOyYMECvd1fdtllepkHHnhAjhw54vXPmR22i6lTp17wum6++Wa/bxcBGT7UNWBUYbJXX31V9u/fL88//7y+Mu7jjz9+0b9VV9MtKSlxTStWrBA7effdd3WdFBW29uzZIwMHDpSUlBQ5fvx4nct/9dVXcvfdd+uw9s033+gvaTV9++23Ymc5OTn6C2XHjh2yZcsWvUMZM2aMrhfTEFWtr/b7/9NPP0kw6Nu3r9vr+uKLL+pdNli3CWXXrl1u60FtG8qdd94Z9NuE2vbV/kB9KdRF7etefPFFva/cuXOn/uJV+47Tp097bX9jh3VRWVmpX8vixYv1zw8++ED/x+XWW2/16ufMLtuFosJG7df1zjvvSEOMbBeWTaxYscJKSkpqcJnf//731p/+9CfLzq677jpr9uzZrtvV1dVWfHy8lZGRUefyd911lzV27Fi3ecnJydZDDz1kBZPjx4+rU8KtnJycepdZs2aNFRUVZQWbJ5980ho4cOAlLx8q24SiPu89evSwampqQmqbUJ+F9evXu26r1x8bG2s988wzrnmlpaVWZGSk9c4773htf2OHdVGXr7/+Wi/3008/ee1zZpd1MWXKFGv8+PEePY6J7SIgez7q4nA4JCYm5qLLvf322/r6Mv369dMXrVMp2C7UYaW8vDzdXeoUHh6ub+fm5tb5N2p+7eUVlVDrW96u1PuvXGwbOHXqlHTt2lVfNGn8+PG65ywYqO5z1a3avXt3uffee+XQoUP1Lhsq24T6vLz11lvy4IMPui5CGUrbRG1FRUW6oGPt911dY0N1l9f3vjdmf2Pn/YfaRuq7blhjPmd2kp2drQ9f9+rVS2bNmiUnT56sd1lT24UtwkdBQYG89NJL8tBDDzW43D333KN3Rp9//rkOHm+++abcd999Yhc///yzVFdXX1AJVt1WO5a6qPmeLG9H6hDcnDlzZOjQoTpU1kd9sF577TXZuHGj3g7U3/3ud7+Tw4cPi52pLxA1dkFVCc7MzNRfNDfeeKO+emSobhOKOrZdWlqqj2mH2jZxPud768n73pj9jR2pw05qDIg6FNnQRdQ8/ZzZxc033yxvvPGGvr7aX/7yF31IOzU1Vb/3/twufFJevT4LFy7UL74h33//vdsgn3//+9965aljumo8R0NmzJjh+l0NNlIDr0aNGiWFhYV60CrsSY39UOMVLnb8VV24sPbFC9WXTJ8+ffTYoaeeekrsSu0onAYMGKB3kup/8u+9954e1xGqVq9erddNQ5fvDtZtApdGjRW766679GBcFShC8XM2efJkt+9F9drU96HqDVHfj/5iNHw8+uijDf4vRVHdXU5qdPKIESP0DuOvf/2rx8+nNh5nz4kdwoc6XNSsWTM5duyY23x1OzY2ts6/UfM9Wd5uHn74Yfnoo4/0iO6EhASP/rZFixZyzTXX6Pc/mKiu4yuvvLLe1xXs24SiBo1u3bpVDyb0RLBuE873Vr3P6j9dTuq2uraWt/Y3dgwealv57LPPPL50/MU+Z3bVvXt3/d6r11VX+DC1XRg97NKxY0fdq9HQFBER4erxUKfODho0SNasWaOPOXkqPz9f/6z9YQxk6rWr16u6x5xUN7G6Xft/b7Wp+bWXV9QZAPUtbxfqfyoqeKxfv17vONRp155SXYf79u2zzft/qdQYBtWbV9/rCtZtoja1T1DHsMeOHevR3wXrNqE+H+qLofb7XlZWps96qe99b8z+xm7BQ43hUCG1ffv2Xv+c2dXhw4f1mI/6Xpex7cIKQIcPH7Z69uxpjRo1Sv9eUlLimmov06tXL2vnzp36dkFBgbVs2TJr9+7dVlFRkbVx40are/fu1rBhwyw7WbdunR6hnpWVZX333XfWjBkzrOjoaOvo0aP6/vvvv99auHCha/kvv/zSat68ufXss89a33//vR6x3aJFC2vfvn2Wnc2aNUufpZCdne32/ldWVrqWOX9dLF261Nq8ebNVWFho5eXlWZMnT7Zatmxp7d+/37KzRx99VK8HtV2r93v06NFWhw4d9BlAobRN1B5536VLF2vBggUX3BfM20R5ebn1zTff6Entup977jn9u/MMjuXLl+t9hdr37d27V5/hoM4Q/M9//uN6jJEjR1ovvfTSJe9v7Lguzpw5Y916661WQkKClZ+f77b/qKqqqnddXOxzZsd1UV5ebs2bN8/Kzc3Vr2vr1q3Wb3/7W+uKK66wTp8+7dftIiDDhzo9Tq3EuiYntSLV7c8//1zfPnTokA4aMTExeqWp8DJ//nzL4XBYdqM2ArVzjYiI0Kc87dixw+10YnXqVG3vvfeedeWVV+rl+/bta3388ceW3dX3/qtto751MWfOHNd669y5s3XLLbdYe/bssexu0qRJVlxcnH5dl19+ub6twnaobRNOKkyobeHAgQMX3BfM24Ta19X1mXC+XnW67eLFi/XrVPtA9Z+389dR165ddRi91P2NHdeF87uhrsn5fVHXurjY58yO66KystIaM2aM1bFjR/0fEPWap0+ffkGI8Md2Eab+8V4/CgAAQBCcagsAAIIH4QMAABhF+AAAAEYRPgAAgFGEDwAAYBThAwAAGEX4AAAARhE+AACAUYQPAABgFOEDAAAYRfgAAABGET4AAICY9D+fbK5pQn1yEQAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# BoxCox\n",
    "y = random_shuffle(np.random.rand(1000) * 10 + 5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(BoxCox)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjEAAAGdCAYAAADjWSL8AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAIeZJREFUeJzt3QuQVNWdP/AfbxAFxJXX+gCNqxAfKEaCWFmjJKhsSko3SmRTaFhJXDVBfMFWVDAmiHHVYAhmXSNmy0dirZJVV1wXg5aKqGASH0jUoGAUiCYMCgEN9L/O3f/MMgjKDD3MnO7Pp+oyfW/f6Tlz6k73l/O4p1WpVCoFAEBmWjd3AQAAGkOIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMhS28jQpk2b4q233orddtstWrVq1dzFAQC2Q7q/7nvvvRd9+vSJ1q1bV2eISQFm7733bu5iAACNsHz58thrr72iKkNMaoGprYQuXbo0d3EAgO2wZs2aohGi9nN8p4eYxx57LL7//e/HwoUL4+2334577703Ro4cWa+p6Iorroibb745Vq9eHUOHDo2ZM2fGAQccUHfOH//4xzj//PPjvvvuK5qTTj311PjBD34Qu+6663aVobYLKQUYIQYA8lKuoSAN7pBau3ZtHHbYYTFjxoytPn/NNdfE9OnT46abbooFCxZE586dY/jw4bF+/fq6c0aPHh0vvvhiPPzww3H//fcXwWjcuHE79psAAFWl1Y6sYp2S1OYtMeml0mCdCy+8MC666KLiWE1NTfTs2TNmzZoVo0aNisWLF8eAAQPimWeeiSOPPLI4Z86cOXHSSSfFm2++WXz/9jRHde3atXhtLTEAkIdyf36XdYr10qVLY8WKFTFs2LC6Y6mwgwcPjvnz5xf76Wu3bt3qAkySzk/dSqnlZms2bNhQ/OKbbwBAdSvrwN4UYJLU8rK5tF/7XPrao0eP+oVo2za6d+9ed86Wpk6dGlOmTGlQWVKr0F/+8pfYuHFjA3+LytemTZuizk1PByBnWcxOmjRpUkyYMOEjo5u35YMPPigGHa9bt24nlTA/u+yyS/Tu3Tvat2/f3EUBgOYPMb169Sq+rly5sviArJX2Bw4cWHfOqlWr6n1fajFJM5Zqv39LHTp0KLbtvRFe6tZKrQ1pfE36kNbiUL+FKoW8P/zhD0U9pVlj5bjhEABkHWL69etXBJG5c+fWhZbUapLGupxzzjnF/pAhQ4qp12mK9qBBg4pjjzzySBE+0tiZHZU+oNNrpZaa1NrAR3Xq1CnatWsXb7zxRlFfHTt2bO4iAUDTh5j3338/Xn311br99L/5X/3qV8WYln322SfGjx8fV111VfE//BRqLrvssqJFpHYGU//+/eOEE06Is88+u5iG/eGHH8Z5551XzFzanplJ20vrwsdTPwBUXYh59tln4/Of/3zdfu1YlTFjxhTTqC+55JLiXjLpvi+pxeWYY44pplBv/r/922+/vQguxx9/fN3N7tK9ZQAAdsp9YlriPPN0U73UOpRagXSTbJt6AiD3+8RkMTupXPpOfGCn/azXrx6xU37O5MmTY/bs2UWXHgBUEwMjMpfujJwGUgNAtamqlphKknoB04380qKZ27twJgBUEi0xLUhaXuGb3/xmcUfjNE4lDYpOa0wl8+bNK+538+CDDxZT09N9cx5//PGiO6l2OjsAVBMtMS1Imtn1H//xH3HbbbfFvvvuW6wInlYA33xK+8SJE+Paa6+N/fbbL3bfffci3ABAg0zuuh3n1ERLJ8S0EGla+syZM4tp6ieeeGJx7Oabb46HH344brnllvjMZz5THLvyyivjC1/4QjOXFgCan+6kFuK1114rbvw3dOjQumPprrpHHXVULF68uO7Y5qt/A0A1E2Iy07lz5+YuAgC0CEJMC7H//vsXi1U+8cQTdcdSy0wa2DtgwIBmLRsAtETGxLSgFpa0SObFF19ctw5VGti7bt26GDt2bPz6179u7iICQItSVSFmZ91Ft7GuvvrqYgXur371q/Hee+8V418eeuihYhYSAFCftZOqlHoCqGKTm2eKdbnXTjImBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhpoVLaykdcsghxYrWI0eObO7iAECLUVXLDmzXHQrL9rMafqfDY489NgYOHBg33HBD3bEJEyYUxx588MHYddddy1xIAMhXdYWYDL322mvxjW98I/baa6/mLgpAVeo78YGs1uGrJrqTWogzzzwzHn300fjBD34QrVq1qtvefffd+NrXvlY8njVrVsybN694nBaGPPzww6NTp05x3HHHxapVq4rWmv79+xfrUZxxxhnFCtgAUKmEmBYihZchQ4bE2WefHW+//Xa8+eabxZYCSepeSsdOP/30uvMnT54cP/zhD+PJJ5+M5cuXx2mnnVacd8cdd8QDDzwQ//3f/x033nhjs/5OANCUdCe1EGlVz/bt28cuu+wSvXr1qjueWl3Sc5sfS6666qoYOnRo8Xjs2LExadKkoutpv/32K479/d//ffzyl7+MSy+9dCf/JgCwc2iJydShhx5a97hnz55F+KkNMLXHUhcTAFQqISZTacr15q01m+/XHtu0aVMzlAwAdg4hpgVJ3UkbN25s7mIAQBaEmBakb9++sWDBgnj99dfjnXfe0ZICAB9DiGlBLrroomjTpk0MGDAg9txzz1i2bFlzFwkAWqxWpVKpFJlZs2ZNMWOnpqammIK8ufXr18fSpUujX79+0bFjx2YrY0unngCq+GZ3k7s2yZ3nd+TzuzG0xAAAWRJiAIAsCTEAQJaEGAAgS0IMAJClig0xGU662qnUDwC5q7gQU3v7/XXr1jV3UVq02vrZcrkCAMhFxa1inW4W161bt7rFD9PCiGkdIf6vBSYFmFQ/qZ5SfQFAjiouxCS9evUqvlrFedtSgKmtJwDIUUWGmNTy0rt37+jRo0d8+OGHzV2cFid1IWmBASB3FRliaqUPah/WAFCZKm5gLwBQHYQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkKWyh5iNGzfGZZddFv369YtOnTrF/vvvH9/5zneiVCrVnZMeX3755dG7d+/inGHDhsUrr7xS7qIAFazvxAfqNqA6lT3ETJs2LWbOnBk//OEPY/HixcX+NddcEzfeeGPdOWl/+vTpcdNNN8WCBQuic+fOMXz48Fi/fn25iwMAVKi25X7BJ598Mk4++eQYMWJEsd+3b9+488474+mnn65rhbnhhhvi29/+dnFe8tOf/jR69uwZs2fPjlGjRpW7SABABSp7S8zRRx8dc+fOjd/+9rfF/q9//et4/PHH48QTTyz2ly5dGitWrCi6kGp17do1Bg8eHPPnz9/qa27YsCHWrFlTbwMAqlvZW2ImTpxYhIyDDjoo2rRpU4yR+e53vxujR48unk8BJkktL5tL+7XPbWnq1KkxZcqUchcVAMhY2Vtifv7zn8ftt98ed9xxRyxatChuu+22uPbaa4uvjTVp0qSoqamp25YvX17WMgMA+Sl7S8zFF19ctMbUjm055JBD4o033ihaU8aMGRO9evUqjq9cubKYnVQr7Q8cOHCrr9mhQ4diAwBospaYdevWRevW9V82dStt2rSpeJymXqcgk8bN1ErdT2mW0pAhQ8pdHACgQpW9JeZLX/pSMQZmn332iU9/+tPx3HPPxXXXXRdf+9rXiudbtWoV48ePj6uuuioOOOCAItSk+8r06dMnRo4cWe7iAAAVquwhJt0PJoWSf/qnf4pVq1YV4eTrX/96cXO7WpdcckmsXbs2xo0bF6tXr45jjjkm5syZEx07dix3cQCAClX2ELPbbrsV94FJ27ak1pgrr7yy2AAAGsPaSQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZKvt9YirC5K7bcU7NzigJALANWmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIkhADAGRJiAEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkCUhBgDIUpOEmN///vfxD//wD7HHHntEp06d4pBDDolnn3227vlSqRSXX3559O7du3h+2LBh8corrzRFUQCAClX2EPOnP/0phg4dGu3atYsHH3wwXnrppfiXf/mX2H333evOueaaa2L69Olx0003xYIFC6Jz584xfPjwWL9+fbmLAwBUqLblfsFp06bF3nvvHbfeemvdsX79+tVrhbnhhhvi29/+dpx88snFsZ/+9KfRs2fPmD17dowaNarcRQIAKlDZW2L+8z//M4488sj48pe/HD169IjDDz88br755rrnly5dGitWrCi6kGp17do1Bg8eHPPnzy93cQCAClX2EPO73/0uZs6cGQcccEA89NBDcc4558Q3v/nNuO2224rnU4BJUsvL5tJ+7XNb2rBhQ6xZs6beBgBUt7J3J23atKloifne975X7KeWmBdeeKEY/zJmzJhGvebUqVNjypQpZS4pAJCzsrfEpBlHAwYMqHesf//+sWzZsuJxr169iq8rV66sd07ar31uS5MmTYqampq6bfny5eUuNgBQ7SEmzUxasmRJvWO//e1vY999960b5JvCyty5c+ueT91DaZbSkCFDtvqaHTp0iC5dutTbAIDqVvbupAsuuCCOPvroojvptNNOi6effjr+9V//tdiSVq1axfjx4+Oqq64qxs2kUHPZZZdFnz59YuTIkeUuDgBQocoeYj7zmc/EvffeW3QBXXnllUVISVOqR48eXXfOJZdcEmvXro1x48bF6tWr45hjjok5c+ZEx44dy10cAKBClT3EJH/3d39XbNuSWmNSwEkbAEBjWDsJAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEtCDACQJSEGAMiSEAMAZEmIAQCyJMQAAFkSYgCALAkxAECWhBgAIEttm7sAADuq78QH6h6/fvWIZi0LsPNoiQEAsiTEAABZEmIAgCwJMQBAloQYACBLQgwAkKUmDzFXX311tGrVKsaPH193bP369XHuuefGHnvsEbvuumuceuqpsXLlyqYuCgBUvsldP3mrEE0aYp555pn48Y9/HIceemi94xdccEHcd999cffdd8ejjz4ab731VpxyyilNWRQAoMI0WYh5//33Y/To0XHzzTfH7rvvXne8pqYmbrnllrjuuuviuOOOi0GDBsWtt94aTz75ZDz11FNNVRwAoMI0WYhJ3UUjRoyIYcOG1Tu+cOHC+PDDD+sdP+igg2KfffaJ+fPnb/W1NmzYEGvWrKm3AQDVrUmWHbjrrrti0aJFRXfSllasWBHt27ePbt261Tves2fP4rmtmTp1akyZMqUpigoAZKrsLTHLly+Pb33rW3H77bdHx44d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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# YeoJohnshon\n",
    "y = random_shuffle(np.random.randn(1000) * 10 + 5)\n",
    "y = np.random.beta(.5, .5, size=1000)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(YeoJohnshon)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAGdCAYAAAA44ojeAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAJoNJREFUeJzt3Q9U1fX9x/E3fwQFBYIJyBHCmhuQmoWlpNtKmWjUycSyzRkWx5ZTF7L8w45RmQvHWppNpXWa2NJZ7kybOC3CpVuiEOWOYZo1/1AIWA5QNxDw/s7nc37fO67Z8vLH+7n3Ph/nfM/3fr/fD/d+7lW5Lz//vj42m80mAAAABvF1dQUAAAAuRkABAADGIaAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABjHX9zQhQsXpKamRvr16yc+Pj6urg4AALgMam3YM2fOSExMjPj6+npeQFHhJDY21tXVAAAAnVBdXS0DBw70vICiWk6sNxgSEuLq6gAAgMvQ1NSkGxis73GPCyhWt44KJwQUAADcy+UMz2CQLAAAMA4BBQAAGIeAAgAAjOOWY1AAAHDlVNm2tjZpb293dVWM4+fnJ/7+/t2yBAgBBQCAy3T+/Hk5efKk/Pvf/3Z1VYwVFBQkAwYMkICAgC49DwEFAIDLXCT06NGjupVALTSmvoBZLNSxZUkFuFOnTunPafDgwV+7GNv/QkABAOAyqC9fFVLUOh6qlQBf1qdPH+nVq5ccP35cf169e/eWzmKQLAAATuhKq4A38O2mz4dPGQAAGIeAAgAAjMMYFAAAuih+0bYr9lrHlqV3y/O888478vDDD8uhQ4ckPT1dtmzZIiahBQUAAA936623SnZ2tsO5nJwcGT58uJ5xU1RUJKYhoAAA4IU++eQTGTt2rAwcOFDCwsLENAQUAAA82IwZM2TXrl3y3HPP6XVbrO2LL76QBx98UD9WLShvv/22fvzGG2/IDTfcoKcMqwBTX18v27dvl8TERAkJCZEf/vCHV2ShOsagAHDL/vzu6ocHPN1zzz0nH330kQwZMkSWLFliX6I/KSlJH0+dOlVCQ0Nl3759+vwTTzwhv/nNb/RaL/fee6/eAgMDZcOGDXL27Fm5++675fnnn5eFCxf2aL0JKAAAeLDQ0FC96q0KHNHR0fbzqrVEXet4Tlm6dKmMHj1aP87KypLc3FzdHXTNNdfoc1OmTJG//vWvPR5Q6OIBAAB2w4YNsz+OiorSwcYKJ9Y51e3T0wgoAADATi1V37GVpeOxdU4t+d/TCCgAAHi4gIAA+9gTd0FAAQDAw8XHx+tBsMeOHZPPP//8irSAdBWDZAEA6CLTZ5U9+uijkpmZqWfu/Oc//9GLs5nOx2az2cTNNDU16ZHHjY2Nek42AO9bEtz0LwR4nubmZv3FPmjQIOndu7erq+OWn5Mz39908QAAAOM4FVDUAJvHHntMpyK1wty1114rTz31lHRshFGP8/LyZMCAAbpMamqqHDlyxOF5Tp8+LdOmTdPpSS2vq+ZZq8VfAAAAnA4ov/zlL2XNmjV6hbkPP/xQHxcUFOgV5SzqeOXKlVJYWKgH5AQHB0taWppu8rGocFJVVSUlJSVSXFwsu3fvloceeog/EQAA4Pwg2T179shdd92lb8tsjQr+wx/+IOXl5fbWkxUrVsjixYt1OeXll1/Wi7qo2zjfd999Otjs2LFDKioqZMSIEbqMCji33367PPPMMxITE+NMlQAAgLe3oNxyyy1SWlqq1/RX/vGPf8jf//53mThxoj5Wg2Jqa2t1t45FDYYZOXKklJWV6WO1V906VjhRVHlfX1/7fQAu1tLSogfWdNwAAHAFN5xb4pafj1MtKIsWLdLhICEhQfz8/PSYlF/84he6y0ZR4URRLSYdqWPrmtpHRkY6VsLfX8LDw+1lLpafny9PPvmkc+8MAIBuZK2oqu7kq8ZY4tKsOx1fvAJtjwaU1157TdavX6/vaHjdddfJ/v37JTs7W3fLqPnVPUXdqCgnJ8d+rEJSbGxsj70eAAAXU/8xVz0A1n1o1D1q1LLv+G/LiQon6vNRn5P6vK5YQJk/f75uRVFjSZShQ4fK8ePHdQuHCijWHRHr6ur0LB6LOh4+fLh+rMpcfJOhtrY2PbPn4jsqWtRtntUGwDvWOAFMZX1PXYmb5bkrFU6+6vu8xwKKSkZqrEhHKiFZS+aq6ceqUmqcihVIVGuHGlsya9YsfZySkiINDQ1SWVkpycnJ+tzOnTv1c6ixKgAAmEq1mKj/gKuhCq2tra6ujnFUt05XW046FVDuvPNOPeYkLi5Od/G8//778uyzz8qDDz5o/4NTXT5Lly6VwYMH68Ci1k1RXUCTJk3SZRITE2XChAkyc+ZMPRVZ/QHPmTNHt8owgwcA4A7Ul3B3fRGjGwKKmg6sAsdPfvIT3bylAsWPf/xjvTCbZcGCBXLu3Dm9rolqKRkzZoyeVtxxuVs1jkWFknHjxukWmYyMDL12CgAAgMK9eAC45RgU7sUDuB/uxQMAANwaAQUAABiHgAIAAIxDQAEAAO49iwcA3GlALgNpAfdFCwoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAAYBwCCgAAMA4BBQAAGIeAAgAAjENAAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHH9XVwCAe4hftM3VVQDgRWhBAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHAIKAAAwDgEFAAAYh4ACAACMQ0ABAADuHVDi4+PFx8fnS9vs2bP19ebmZv04IiJC+vbtKxkZGVJXV+fwHCdOnJD09HQJCgqSyMhImT9/vrS1tXXvuwIAAN4TUCoqKuTkyZP2raSkRJ+/55579H7evHmydetW2bRpk+zatUtqampk8uTJ9p9vb2/X4eT8+fOyZ88eWbdunRQVFUleXl53vy8AAODGfGw2m62zP5ydnS3FxcVy5MgRaWpqkv79+8uGDRtkypQp+vqhQ4ckMTFRysrKZNSoUbJ9+3a54447dHCJiorSZQoLC2XhwoVy6tQpCQgIuKzXVa8VGhoqjY2NEhIS0tnqA/Dwe/EcW5bu6ioA6OT3d6fHoKhWkFdeeUUefPBB3c1TWVkpra2tkpqaai+TkJAgcXFxOqAoaj906FB7OFHS0tJ0hauqqr7ytVpaWnSZjhsAAPBcnQ4oW7ZskYaGBpkxY4Y+rq2t1S0gYWFhDuVUGFHXrDIdw4l13br2VfLz83XisrbY2NjOVhsAAHhyQHnppZdk4sSJEhMTIz0tNzdXNwdZW3V1dY+/JgAAcB3/zvzQ8ePH5a233pI//elP9nPR0dG620e1qnRsRVGzeNQ1q0x5ebnDc1mzfKwylxIYGKg3AOjucTOMUwE8qAVl7dq1eoqwmpFjSU5Oll69eklpaan93OHDh/W04pSUFH2s9gcOHJD6+np7GTUTSA2USUpK6to7AQAA3tuCcuHCBR1QMjMzxd//vz+uxoZkZWVJTk6OhIeH69Axd+5cHUrUDB5l/PjxOohMnz5dCgoK9LiTxYsX67VTaCEBAACdDiiqa0e1iqjZOxdbvny5+Pr66gXa1MwbNUNn9erV9ut+fn56WvKsWbN0cAkODtZBZ8mSJc5WAwAAeLAurYPiKqyDAlx57rgOyuVgDArgYeugAAAA9BQCCgAAMA4BBQAAGIeAAgAAjENAAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHAIKAABw/3vxAPA8nrqMPQD3RQsKAAAwDgEFAAAYh4ACAACMQ0ABAADGIaAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAONwN2MAXu1y7uR8bFn6FakLgP+iBQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAA4P4B5bPPPpMf/ehHEhERIX369JGhQ4fKu+++a79us9kkLy9PBgwYoK+npqbKkSNHHJ7j9OnTMm3aNAkJCZGwsDDJysqSs2fPds87AgAA3hVQ/vWvf8no0aOlV69esn37djl48KD8+te/lquuuspepqCgQFauXCmFhYWyb98+CQ4OlrS0NGlubraXUeGkqqpKSkpKpLi4WHbv3i0PPfRQ974zAADgtnxsqsnjMi1atEjeeecd+dvf/nbJ6+qpYmJi5Gc/+5k8+uij+lxjY6NERUVJUVGR3HffffLhhx9KUlKSVFRUyIgRI3SZHTt2yO233y6ffvqp/vmv09TUJKGhofq5VSsMgJ5fTdWbsZIs0D2c+f52qgXlz3/+sw4V99xzj0RGRsoNN9wgL774ov360aNHpba2VnfrWFRFRo4cKWVlZfpY7VW3jhVOFFXe19dXt7hcSktLi35THTcAAOC5nAoo//znP2XNmjUyePBgeeONN2TWrFny05/+VNatW6evq3CiqBaTjtSxdU3tVbjpyN/fX8LDw+1lLpafn6+DjrXFxsY69y4BAIDnBpQLFy7IjTfeKE8//bRuPVHjRmbOnKnHm/Sk3Nxc3RxkbdXV1T36egAAwI0CipqZo8aPdJSYmCgnTpzQj6Ojo/W+rq7OoYw6tq6pfX19vcP1trY2PbPHKnOxwMBA3VfVcQMAAJ7LqYCiZvAcPnzY4dxHH30kV199tX48aNAgHTJKS0vt19V4ETW2JCUlRR+rfUNDg1RWVtrL7Ny5U7fOqLEqAAAA/s4Unjdvntxyyy26i+fee++V8vJy+e1vf6s3xcfHR7Kzs2Xp0qV6nIoKLI899piemTNp0iR7i8uECRPsXUOtra0yZ84cPcPncmbwAAAAz+dUQLnppptk8+bNekzIkiVLdABZsWKFXtfEsmDBAjl37pwen6JaSsaMGaOnEffu3dteZv369TqUjBs3Ts/eycjI0GunAAAAOL0OiilYBwXoXqyD8r+xDgpg+DooAAAAVwIBBQAAGIeAAgAAjENAAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHAIKAAAwDgEFAAAYh4ACAACMQ0ABAADGIaAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABjH39UVANCz4hdtc3UVAMBptKAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABiHacYA0A1TtY8tS78idQG8BS0oAADAOAQUAABgHAIKAAAwDgEFAAAYh4ACAADcO6A88cQT4uPj47AlJCTYrzc3N8vs2bMlIiJC+vbtKxkZGVJXV+fwHCdOnJD09HQJCgqSyMhImT9/vrS1tXXfOwIAAN43zfi6666Tt956679P4P/fp5g3b55s27ZNNm3aJKGhoTJnzhyZPHmyvPPOO/p6e3u7DifR0dGyZ88eOXnypNx///3Sq1cvefrpp7vrPQEAAG8LKCqQqIBxscbGRnnppZdkw4YNMnbsWH1u7dq1kpiYKHv37pVRo0bJm2++KQcPHtQBJyoqSoYPHy5PPfWULFy4ULfOBAQEdM+7AgAA3jUG5ciRIxITEyPXXHONTJs2TXfZKJWVldLa2iqpqan2sqr7Jy4uTsrKyvSx2g8dOlSHE0taWpo0NTVJVVXVV75mS0uLLtNxAwAAnsupgDJy5EgpKiqSHTt2yJo1a+To0aPyne98R86cOSO1tbW6BSQsLMzhZ1QYUdcUte8YTqzr1rWvkp+fr7uMrC02NtaZagMAAE/u4pk4caL98bBhw3Rgufrqq+W1116TPn36SE/Jzc2VnJwc+7FqQSGkAADgubo0zVi1lnzrW9+Sjz/+WI9LOX/+vDQ0NDiUUbN4rDEran/xrB7r+FLjWiyBgYESEhLisAEAAM/VpYBy9uxZ+eSTT2TAgAGSnJysZ+OUlpbarx8+fFiPUUlJSdHHan/gwAGpr6+3lykpKdGBIykpqStVAQAA3trF8+ijj8qdd96pu3Vqamrk8ccfFz8/P/nBD36gx4ZkZWXprpjw8HAdOubOnatDiZrBo4wfP14HkenTp0tBQYEed7J48WK9dopqJQEAAHA6oHz66ac6jHzxxRfSv39/GTNmjJ5CrB4ry5cvF19fX71Am5p5o2borF692v7zKswUFxfLrFmzdHAJDg6WzMxMWbJkCX8aAADAzsdms9nEzahBsqrFRq29wngU4H+LX7TN1VXwCseWpbu6CoBHfX9zLx4AAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAAYBwCCgAAMA4BBQAAGIeAAgAAjENAAQAAxiGgAAAA4/i7ugIAOi9+0TZXVwEAegQtKAAAwDgEFAAAYBwCCgAAMA5jUADgCo0HOrYs/YrUBfAEtKAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAA4FkBZdmyZeLj4yPZ2dn2c83NzTJ79myJiIiQvn37SkZGhtTV1Tn83IkTJyQ9PV2CgoIkMjJS5s+fL21tbV2pCgAA8CCdDigVFRXywgsvyLBhwxzOz5s3T7Zu3SqbNm2SXbt2SU1NjUyePNl+vb29XYeT8+fPy549e2TdunVSVFQkeXl5XXsnAADAuwPK2bNnZdq0afLiiy/KVVddZT/f2NgoL730kjz77LMyduxYSU5OlrVr1+ogsnfvXl3mzTfflIMHD8orr7wiw4cPl4kTJ8pTTz0lq1at0qEFAACgUwFFdeGoVpDU1FSH85WVldLa2upwPiEhQeLi4qSsrEwfq/3QoUMlKirKXiYtLU2ampqkqqqq8+8EAAB4DH9nf2Djxo3y3nvv6S6ei9XW1kpAQICEhYU5nFdhRF2zynQMJ9Z169qltLS06M2iwgwAAPBcTrWgVFdXyyOPPCLr16+X3r17y5WSn58voaGh9i02NvaKvTYAADA8oKgunPr6ernxxhvF399fb2og7MqVK/Vj1RKixpE0NDQ4/JyaxRMdHa0fq/3Fs3qsY6vMxXJzc/X4FmtTQQkAAHgupwLKuHHj5MCBA7J//377NmLECD1g1nrcq1cvKS0ttf/M4cOH9bTilJQUfaz26jlU0LGUlJRISEiIJCUlXfJ1AwMD9fWOGwAA8FxOjUHp16+fDBkyxOFccHCwXvPEOp+VlSU5OTkSHh6ug8TcuXN1KBk1apS+Pn78eB1Epk+fLgUFBXrcyeLFi/XAWxVEAAAAnB4k+3WWL18uvr6+eoE2NbBVzdBZvXq1/bqfn58UFxfLrFmzdHBRASczM1OWLFnS3VUBAABuysdms9nEzahZPGqwrBqPQncPvFn8om2urgKccGxZuqurALjN9zf34gEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAAYBwCCgAAMA4BBQAAGIeAAgAAjENAAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHAIKAAAwDgEFAAAYh4ACAACMQ0ABAADGIaAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMbxd3UFAFxa/KJtrq4CALgMAQUADAqdx5alX5G6AKajiwcAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHsHlDVr1siwYcMkJCREbykpKbJ9+3b79ebmZpk9e7ZERERI3759JSMjQ+rq6hye48SJE5Keni5BQUESGRkp8+fPl7a2tu57RwAAwLsCysCBA2XZsmVSWVkp7777rowdO1buuusuqaqq0tfnzZsnW7dulU2bNsmuXbukpqZGJk+ebP/59vZ2HU7Onz8ve/bskXXr1klRUZHk5eV1/zsDAABuy8dms9m68gTh4eHyq1/9SqZMmSL9+/eXDRs26MfKoUOHJDExUcrKymTUqFG6teWOO+7QwSUqKkqXKSwslIULF8qpU6ckICDgsl6zqalJQkNDpbGxUbfkAJ6Ihdq8E+ugwJM1OfH93ekxKKo1ZOPGjXLu3Dnd1aNaVVpbWyU1NdVeJiEhQeLi4nRAUdR+6NCh9nCipKWl6QpbrTCX0tLSost03AAAgOdyOqAcOHBAjy8JDAyUhx9+WDZv3ixJSUlSW1urW0DCwsIcyqswoq4pat8xnFjXrWtfJT8/Xycua4uNjXW22gAAwJMDyre//W3Zv3+/7Nu3T2bNmiWZmZly8OBB6Um5ubm6Ocjaqqure/T1AACAm92LR7WSfPOb39SPk5OTpaKiQp577jmZOnWqHvza0NDg0IqiZvFER0frx2pfXl7u8HzWLB+rzKWo1hq1AQAA79DldVAuXLigx4iosNKrVy8pLS21Xzt8+LCeVqzGqChqr7qI6uvr7WVKSkr0QBnVTQQAAOB0C4rqapk4caIe+HrmzBk9Y+ftt9+WN954Q48NycrKkpycHD2zR4WOuXPn6lCiZvAo48eP10Fk+vTpUlBQoMedLF68WK+dQgsJAADoVEBRLR/333+/nDx5UgcStWibCiff//739fXly5eLr6+vXqBNtaqoGTqrV6+2/7yfn58UFxfrsSsquAQHB+sxLEuWLHGmGgAAwMN1eR0UV2AdFHgD1kHxTqyDAk/WdCXWQQEAAOgpBBQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgPvfLBAA4NoF+ljMDd6AFhQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAAYBxWkgUMXS0UALwZLSgAAMA4BBQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAAYBwCCgAAMA4BBQAAuHdAyc/Pl5tuukn69esnkZGRMmnSJDl8+LBDmebmZpk9e7ZERERI3759JSMjQ+rq6hzKnDhxQtLT0yUoKEg/z/z586Wtra173hEAAPCugLJr1y4dPvbu3SslJSXS2toq48ePl3PnztnLzJs3T7Zu3SqbNm3S5WtqamTy5Mn26+3t7TqcnD9/Xvbs2SPr1q2ToqIiycvL6953BgAA3JaPzWazdfaHT506pVtAVBD57ne/K42NjdK/f3/ZsGGDTJkyRZc5dOiQJCYmSllZmYwaNUq2b98ud9xxhw4uUVFRukxhYaEsXLhQP19AQMDXvm5TU5OEhobq1wsJCels9QGXiV+0zdVVgBs7tizd1VUAOsWZ7+8ujUFRL6CEh4frfWVlpW5VSU1NtZdJSEiQuLg4HVAUtR86dKg9nChpaWm60lVVVZd8nZaWFn294wYAADxXpwPKhQsXJDs7W0aPHi1DhgzR52pra3ULSFhYmENZFUbUNatMx3BiXbeufdXYF5W4rC02Nraz1QYAAJ4cUNRYlA8++EA2btwoPS03N1e31lhbdXV1j78mAABwHf/O/NCcOXOkuLhYdu/eLQMHDrSfj46O1oNfGxoaHFpR1Cwedc0qU15e7vB81iwfq8zFAgMD9QYAALyDUy0oajytCiebN2+WnTt3yqBBgxyuJycnS69evaS0tNR+Tk1DVtOKU1JS9LHaHzhwQOrr6+1l1IwgNVgmKSmp6+8IAAB4VwuK6tZRM3Ref/11vRaKNWZEjQvp06eP3mdlZUlOTo4eOKtCx9y5c3UoUTN4FDUtWQWR6dOnS0FBgX6OxYsX6+emlQQAADgdUNasWaP3t956q8P5tWvXyowZM/Tj5cuXi6+vr16gTc2+UTN0Vq9ebS/r5+enu4dmzZqlg0twcLBkZmbKkiVL+BMBAABdXwfFVVgHBe6OdVDQFayDAnd1xdZBAQAA6AkEFAAAYBwCCgAAMA4BBQAAeMZCbQAAswdZM5AW7o6AAnQzZugAQNfRxQMAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDgEFAAAYBwCCgAAMA4BBQAAGIeAAgAAjENAAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHAIKAAAwDgEFAAAYh4ACAACMQ0ABAADGIaAAAADjEFAAAIBx/F1dAcCdxC/a5uoqAIBXoAUFAAAYh4ACAACMQxcPAHhpd+SxZelXpC5AZ9CCAgAAjENAAQAA7h9Qdu/eLXfeeafExMSIj4+PbNmyxeG6zWaTvLw8GTBggPTp00dSU1PlyJEjDmVOnz4t06ZNk5CQEAkLC5OsrCw5e/Zs198NAADwzoBy7tw5uf7662XVqlWXvF5QUCArV66UwsJC2bdvnwQHB0taWpo0Nzfby6hwUlVVJSUlJVJcXKxDz0MPPdS1dwIAALx3kOzEiRP1dimq9WTFihWyePFiueuuu/S5l19+WaKionRLy3333Scffvih7NixQyoqKmTEiBG6zPPPPy+33367PPPMM7plBgAAeLduHYNy9OhRqa2t1d06ltDQUBk5cqSUlZXpY7VX3TpWOFFUeV9fX93iciktLS3S1NTksAEAAM/VrQFFhRNFtZh0pI6ta2ofGRnpcN3f31/Cw8PtZS6Wn5+vg461xcbGdme1AQCAYdxiFk9ubq40Njbat+rqaldXCQAAuEtAiY6O1vu6ujqH8+rYuqb29fX1Dtfb2tr0zB6rzMUCAwP1jJ+OGwAA8FzdGlAGDRqkQ0Zpaan9nBovosaWpKSk6GO1b2hokMrKSnuZnTt3yoULF/RYFQAAAKdn8aj1Sj7++GOHgbH79+/XY0ji4uIkOztbli5dKoMHD9aB5bHHHtMzcyZNmqTLJyYmyoQJE2TmzJl6KnJra6vMmTNHz/BhBg8AAOhUQHn33Xfltttusx/n5OTofWZmphQVFcmCBQv0WilqXRPVUjJmzBg9rbh37972n1m/fr0OJePGjdOzdzIyMvTaKQAAAIqPTS1e4mZUt5GazaMGzDIeBabdgA1wF9wsECZ/f7vFLB4AAOBdCCgAAMA4BBQAAOD+g2QBAN4zpopxKnAVWlAAAIBxCCgAAMA4BBQAAGAcAgoAADAOg2SB/8cibABgDlpQAACAcQgoAADAOAQUAABgHAIKAAAwDgEFAAAYh1k8AICvxHL4cBVaUAAAgHFoQYFXYI0TAHAvtKAAAADjEFAAAIBxCCgAAMA4BBQAAGAcBskCALqEqcjoCbSgAAAA49CCArfHFGIA8Dy0oAAAAOMQUAAAgHEIKAAAwDiMQYHRGF8CAN6JgAIA6HFMRYaz6OIBAADGIaAAAADjuDSgrFq1SuLj46V3794ycuRIKS8vd2V1AACAt49BefXVVyUnJ0cKCwt1OFmxYoWkpaXJ4cOHJTIy0lXVwhXEAFgAHTFOBUa0oDz77LMyc+ZMeeCBByQpKUkHlaCgIPnd737nqioBAABvbkE5f/68VFZWSm5urv2cr6+vpKamSllZ2ZfKt7S06M3S2Nio901NTT1SvyGPv/G1ZT54Mk08UXe998t5HgBwVty8Td3yPJ76O9x01ve2zWYzM6B8/vnn0t7eLlFRUQ7n1fGhQ4e+VD4/P1+efPLJL52PjY0VVwldIV7Lm987AM/A7zHXOnPmjISGhrr/OiiqpUWNV7FcuHBBTp8+LREREeLj4+PSurlDWlVBrrq6WkJCQlxdHY/B59pz+Gx7Dp9tz+GzvTyq5USFk5iYmK8t65KA8o1vfEP8/Pykrq7O4bw6jo6O/lL5wMBAvXUUFhbW4/X0JOofDP9ouh+fa8/hs+05fLY9h8/2631dy4lLB8kGBARIcnKylJaWOrSKqOOUlBRXVAkAABjEZV08qssmMzNTRowYITfffLOeZnzu3Dk9qwcAAHg3lwWUqVOnyqlTpyQvL09qa2tl+PDhsmPHji8NnEXXqK6xxx9//EtdZOgaPteew2fbc/hsew6fbffzsV3OXB8AAIAriHvxAAAA4xBQAACAcQgoAADAOAQUAABgHAKKF1L3NVKzptQqvPv373d1ddzesWPHJCsrSwYNGiR9+vSRa6+9Vo/mV/ecgvNWrVol8fHx0rt3b32n8/LycldXya2pW4XcdNNN0q9fP32n+EmTJum7xqP7LVu2TP9ezc7OdnVVPAIBxQstWLDgspYZxuVR949SCw2+8MILUlVVJcuXL9d35/75z3/u6qq5nVdffVWvkaQC3nvvvSfXX3+9pKWlSX19vaur5rZ27dols2fPlr1790pJSYm0trbK+PHj9bpT6D4VFRX6d8CwYcNcXRXPoaYZw3v85S9/sSUkJNiqqqrU9HLb+++/7+oqeaSCggLboEGDXF0Nt3PzzTfbZs+ebT9ub2+3xcTE2PLz811aL09SX1+v/+3v2rXL1VXxGGfOnLENHjzYVlJSYvve975ne+SRR1xdJY9AC4oXUfc6mjlzpvz+97+XoKAgV1fHozU2Nkp4eLirq+FWVJdYZWWlpKam2s/5+vrq47KyMpfWzdP+bir8/ew+qoUqPT3d4e8uus4t7maMrlPr8c2YMUMefvhhfXsBNW4CPePjjz+W559/Xp555hlXV8WtfP7559Le3v6l1aTVsepGQ9eprkg1PmL06NEyZMgQV1fHI2zcuFF3R6ouHnQvWlDc3KJFi/SgrP+1qV/u6gtT3eI6NzfX1VX2uM+2o88++0wmTJgg99xzj26tAkz7n/4HH3ygv1TRddXV1fLII4/I+vXr9aBudC+Wundz6n5GX3zxxf8sc80118i9994rW7du1V+qFvW/VT8/P5k2bZqsW7fuCtTWMz9bdXdupaamRm699VYZNWqUFBUV6e4JONfFo7oe//jHP+qZJhZ1U9GGhgZ5/fXXXVo/dzdnzhz9Ge7evVvPOEPXbdmyRe6++279e7Tj71X1e1b9+1czJjteg3MIKF7ixIkT0tTUZD9WX6ZqdoT6MlBTOQcOHOjS+rk71XJy2223SXJysrzyyiv8Uuok9XdR3d1ctfhZXRJxcXH6y1W1aMF56lf83LlzZfPmzfL222/L4MGDXV0lj6FapY8fP+5w7oEHHpCEhARZuHAh3WhdxBgUL6F+yXfUt29fvVdrdhBOuh5OVMvJ1VdfrcedqJYXS3R0tEvr5m7UFGPVYqLGSamgsmLFCj0dVv3SR+e7dTZs2KBbT9RaKOru8UpoaKhetwedpz7Pi0NIcHCwREREEE66AQEF6CK1toQaGKu2i8MeDZTOmTp1qg54eXl5+otULSi4Y8eOLw2cxeVbs2aN3qsQ3dHatWv1wHnAVHTxAAAA4zCKDwAAGIeAAgAAjENAAQAAxiGgAAAA4xBQAACAcQgoAADAOAQUAABgHAIKAAAwDgEFAAAYh4ACAACMQ0ABAADGIaAAAAAxzf8Bvi6OGk92jA8AAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# QuantileTransformer\n",
    "y = - np.random.beta(1, .5, 10000) * 10\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(Quantile)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "plt.hist(y, 50, label='ori',)\n",
    "y_tfm = preprocessor.transform(y)\n",
    "plt.legend(loc='best')\n",
    "plt.show()\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y, 1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "def ReLabeler(cm):\n",
    "    r\"\"\"Changes the labels in a dataset based on a dictionary (class mapping)\n",
    "        Args:\n",
    "            cm = class mapping dictionary\n",
    "    \"\"\"\n",
    "    def _relabel(y):\n",
    "        obj = len(set([len(listify(v)) for v in cm.values()])) > 1\n",
    "        keys = cm.keys()\n",
    "        if obj:\n",
    "            new_cm = {k:v for k,v in zip(keys, [listify(v) for v in cm.values()])}\n",
    "            return np.array([new_cm[yi] if yi in keys else listify(yi) for yi in y], dtype=object).reshape(*y.shape)\n",
    "        else:\n",
    "            new_cm = {k:v for k,v in zip(keys, [listify(v) for v in cm.values()])}\n",
    "            return np.array([new_cm[yi] if yi in keys else listify(yi) for yi in y]).reshape(*y.shape)\n",
    "    return _relabel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(array(['d', 'd', 'a', 'd', 'b', 'e', 'a', 'd', 'b', 'c', 'b', 'e', 'b',\n",
       "        'b', 'a', 'e', 'd', 'e', 'c', 'e'], dtype='<U1'),\n",
       " array(['z', 'z', 'x', 'z', 'x', 'z', 'x', 'z', 'x', 'y', 'x', 'z', 'x',\n",
       "        'x', 'x', 'z', 'z', 'z', 'y', 'z'], dtype='<U1'))"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vals = {0:'a', 1:'b', 2:'c', 3:'d', 4:'e'}\n",
    "y = np.array([vals[i] for i in np.random.randint(0, 5, 20)])\n",
    "labeler = ReLabeler(dict(a='x', b='x', c='y', d='z', e='z'))\n",
    "y_new = labeler(y)\n",
    "test_eq(y.shape, y_new.shape)\n",
    "y, y_new"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/javascript": "IPython.notebook.save_checkpoint();",
      "text/plain": [
       "<IPython.core.display.Javascript object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/Users/nacho/notebooks/tsai/nbs/009_data.preprocessing.ipynb saved at 2025-07-29 10:40:05\n",
      "Correct notebook to script conversion! 😃\n",
      "Tuesday 29/07/25 10:40:08 CEST\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "\n",
       "                <audio  controls=\"controls\" autoplay=\"autoplay\">\n",
       "                    <source src=\"data:audio/wav;base64,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\" type=\"audio/wav\" />\n",
       "                    Your browser does not support the audio element.\n",
       "                </audio>\n",
       "              "
      ],
      "text/plain": [
       "<IPython.lib.display.Audio object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#|eval: false\n",
    "#|hide\n",
    "from tsai.export import get_nb_name; nb_name = get_nb_name(locals())\n",
    "from tsai.imports import create_scripts; create_scripts(nb_name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
